12.19 HTML Canvas绘制图像
前面我们讲解了如何创建图形(shape)以及加载图像(image),这一节我们讲述如何通过像素操作从零开始创建图像。
要创建一些像素,需要调用2D渲染上下文的createImageData
方法。通过传入宽度和高度,它会返回一个包含所有常规属性的ImageData对象:width、height和(最重要的)data。data属性所包含的CanvasPixelArray
将保存新的像素,此时它们是不可见的,因为它们都被设置为透明黑色。
在下一个例子中,我们将创建一个包含200×200透明像素区域的ImageData
对象,然后将它们全部修改成红色。
var imageData = context.createImageData(200, 200); var pixels = imageData.data;
变量pixels仅用作访问CanvasPixelArray中的像素的快捷方式。
修改颜色值与查询颜色值一样简单:都是读写CanvasPixelArray中的颜色值。如果想将所有像素修改为红色,那么需要使用for循环语句遍历每一个像素。
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 };
变量numPixels保存了ImageData对象中的像素个数,它就是for循环的执行次数。在每一次循环过程中,我们都使用一个简单算法给每个像素赋予颜色值。每个像素都有4个颜色值,所以将像素个数乘以4就能够得到该像素的红色颜色值在CanvasPixelArray中的索引位置。然后,就可以将红色颜色值设置为255(全色),绿色和蓝色设置为0,而阿尔法值设置为255,这样它就变成不透明的r。非常简单!
按照目前情况,我们所做的就是创建一个ImageData,然后将像素修改为红色。现在画布上还看不见任何效果,因为我们还没有将新像素画到上面。为此,我们需要调用2D渲染上下文的putImageData
方法。这个方法可以接受3个或7个参数:ImageData对象、绘制像素数据的原点坐标(x,y)、所谓脏矩形(dirty rectangle)的原点坐标(x,y),脏矩形的宽度和高度。在这个例子中,你暂时可以不考虑脏矩形的用途,它的作用只是定义ImageData对象中需要绘制的像素。
context.putImageData(imageData, 0, 0);
这样会在画布原点绘制新的红色像素(参见图1)。
随机绘制像素
只有红色像素似乎太单调,让我们更进一步,绘制一些完全随机的颜色。这也很简单。
for (var i = 0; i < numPixels; i++) { pixels[i*4] = Math.floor(Math.random()*255); // Red pixels[i*4+1] = Math.floor(Math.random()*255); // Green pixels[i*4+2] = Math.floor(Math.random()*255); // Blue pixels[i*4+3] = 255; // Alpha };
通过修改前一个例子中设置颜色值的代码,我们可以插入0至255之间的随机数。我们仍然保持阿尔法值为255,否则有一些像素会变成透明的。注意,我们使用了Math.floor
来向下舍入产生的随机数(例如,150.456会变成150)。
结果,我们得到一些杂乱的像素点(参见图2)。
注意:Math.random可以产生0到1之间的随机小数.将它与另一个数字相乘,就可以得到0与该数字(乘数)之间的随机数。例如,Math.random()*255将得到0与255之间的一个随机数。
创建马赛克效果
但是,杂乱的像素并不是画布的最佳用途。那么创建一个马赛克效果呢?肯定更有意思一些。它的实现方法是,创建一个新像素区域,然后将它分割到一个栅格中,并为栅格每个片段设置随机颜色。最复杂的部分是计算出每个像素应该落到哪个片段,这样相同的片段就可以设置相同的颜色。在图3中,我们会看到每个片段实际上是由许多像素构成的。
稍后,我会介绍如何计算出每个片段的像素。现在,先来做一些基础性工作。
var imageData = context.createImageData(500, 500); var pixels = imageData.data; // Number of mosaic tiles var numTileRows = 4; var numTileCols = 4; // Dimensions of each tile var tileWidth = imageData.width/numTileCols; var tileHeight = imageData.height/numTileRows;
前两行代码现在你应该很熟悉了,它们创建了一个500×500像素的ImageData对象,然后将CanvasPixelArray保存在一个变量中。后面的代码是定义两个变量,用于声明像素区域划分的片段数,其中包括每行每列的马赛克数。从现在起,我们将片段称为块(tile),因为这个词更能说明它们的实际作用。最后两行代码是根据ImageData对象的尺寸和各行各列的块数计算出每个块的宽度和高度(以像素为单位)。
现在,我们有了足够信息,可以开始遍历这些块和修改像素的颜色值。
for (var r = 0; r < numTileRows; r++) { for (var c = 0; c < numTileCols; c++) { // Set the pixel values for each tile var red = Math.floor(Math.random()*255); var green = Math.floor(Math.random()*255); var blue = Math.floor(Math.random()*255); }; };
这是一个嵌套循环,第一个循环遍历每一行的块,第二个循环遍历当前行的每一列块(参见图4左边的栅格)。每一个块都赋了新的颜色值,这些值都是0至255的随机数。到现在为止,所有代码都是非常基础的。
现在,在列循环中颜色值的下方,我们要声明另外两个循环:
for (var tr = 0; tr < tileHeight; tr++) { for (var tc = 0; tc < tileWidth; tc++){ }; };
根据之前计算的块尺寸,这些循环遍历的次数与每个块中的像素个数相同。变量tr和tc表示当前访问块的像素行(基于块的高度)和像素列(基于块的宽度)(参见图4右边的栅格)。在这个例子中,每一个块的宽和高都是125像素,所以tr将会循环125次,而在每一次循环中,tc将会再循环125次。
然而,我们现在仍然还无法访问每一个块中的实际像素。我们现在得到的是所访问的块的行和列(变量r和c),以及你在该块中所处的像素的行和列(变量tr和tc)对于它们本身而言,这些变量并不足以用来访问CanvasPixelArray中的像素。 为此,需要将它们转换为以0开始的像素位置坐标(x,y),就像是没有块存在时那样。将下面的代码添加到第二个循环中,然后我将解释会出现什么结果,这事实上是很简单的:
var trueX = (c*tileWidth)+tc; var trueY = (r*tileHeight)+tr;
这两个变量可以计算出像素的真实位置。例如,要计算x轴位置,首先要将当前块的列数(2)乘以每个块的宽度(125),这样就得到所访问块的左边缘的x坐标位置(2×125=250)。然后,再加上所访问的块中像素的列数(例如,10),这样就得到没有块时的x轴确切坐标(250+ 10= 260)。对y轴重复这个过程,就可以得到开始修改像素颜色值的位置坐标(x,y)。将下面的代码加到trueX和trueY的赋值语句后面:
var pos = (trueY*(imageData.width*4))+(trueX*4); pixels[pos] = red; pixels[pos+1] = green; pixels[pos+2] = blue; pixels[pos+3] = 255;
这里并没有出现新代码,它只是访问像素的红色颜色值,然后使用之前设置的颜色值进行赋值。因为这里从0开始计算,所以必须将trueX和trueY减1。最后一步是将像素绘制到画布上,所以要将下面的putImageData调用放到4个循环之外:
context.putImageData(imageData, 0, 0);
如果一切正常,画布上就会出现生动的马赛克效果(参见图5)。
通过修改每行和每列的块数,还能创建出更有趣的效果(参见图6)。
$(document).ready(function () { var canvas1 = $("#canvas1"); var context1 = canvas1.get(0).getContext("2d"); var imageData1 = context1.createImageData(200, 200); var pixels1 = imageData1.data; var numPixels1 = imageData1.width * imageData1.height; for (var i = 0; i < numPixels1; i++) { pixels1[i * 4] = 255; // Red pixels1[i * 4 + 1] = 0; // Green pixels1[i * 4 + 2] = 0; // Blue pixels1[i * 4 + 3] = 255; // Alpha }; context1.putImageData(imageData1, 0, 0); var canvas2 = $("#canvas2"); var context2 = canvas2.get(0).getContext("2d"); var imageData2 = context2.createImageData(200, 200); var pixels2 = imageData2.data; var numPixels2 = imageData2.width * imageData2.height; for (var i = 0; i < numPixels2; i++) { pixels2[i * 4] = Math.floor(Math.random() * 255); // Red pixels2[i * 4 + 1] = Math.floor(Math.random() * 255); // Green pixels2[i * 4 + 2] = Math.floor(Math.random() * 255); // Blue pixels2[i * 4 + 3] = 255; // Alpha }; context2.putImageData(imageData2, 0, 0); var canvas3 = $("#canvas3"); var context3 = canvas3.get(0).getContext("2d"); // Creating a mosaic var imageData3 = context3.createImageData(500, 500); var pixels3 = imageData3.data; // Number of mosaic segments var numTileRows = 4; var numTileCols = 4; // Dimensions of each segment var tileWidth = imageData3.width / numTileCols; var tileHeight = imageData3.height / numTileRows; // Loop through each tile for (var r = 0; r < numTileRows; r++) { for (var c = 0; c < numTileCols; c++) { // Set the pixel values for each tile var red = Math.floor(Math.random() * 255); var green = Math.floor(Math.random() * 255); var blue = Math.floor(Math.random() * 255); // Loop through each tile pixel for (var tr = 0; tr < tileHeight; tr++) { for (var tc = 0; tc < tileWidth; tc++) { // Calculate the true position of the tile pixel var trueX = (c * tileWidth) + tc; var trueY = (r * tileHeight) + tr; // Calculate the position of the current pixel in the array var pos = (trueY * (imageData3.width * 4)) + (trueX * 4); // Assign the colour to each pixel pixels3[pos] = red; pixels3[pos + 1] = green; pixels3[pos + 2] = blue; pixels3[pos + 3] = 255; }; }; }; }; // Draw image data to the canvas context3.putImageData(imageData3, 0, 0); var canvas4 = $("#canvas4"); var context4 = canvas4.get(0).getContext("2d"); // Creating a mosaic var imageData4 = context4.createImageData(500, 500); var pixels4 = imageData4.data; // Number of mosaic segments var numTileRows = 20; var numTileCols = 20; // Dimensions of each segment var tileWidth = imageData4.width / numTileCols; var tileHeight = imageData4.height / numTileRows; // Loop through each tile for (var r = 0; r < numTileRows; r++) { for (var c = 0; c < numTileCols; c++) { // Set the pixel values for each tile var red = Math.floor(Math.random() * 255); var green = Math.floor(Math.random() * 255); var blue = Math.floor(Math.random() * 255); // Loop through each tile pixel for (var tr = 0; tr < tileHeight; tr++) { for (var tc = 0; tc < tileWidth; tc++) { // Calculate the true position of the tile pixel var trueX = (c * tileWidth) + tc; var trueY = (r * tileHeight) + tr; // Calculate the position of the current pixel in the array var pos = (trueY * (imageData4.width * 4)) + (trueX * 4); // Assign the colour to each pixel pixels4[pos] = red; pixels4[pos + 1] = green; pixels4[pos + 2] = blue; pixels4[pos + 3] = 255; }; }; }; }; // Draw image data to the canvas context4.putImageData(imageData4, 0, 0); });