今天要介绍的是canvas对图形对象的操作,包括图像、视频绘制,和操作像素对象的方法。
图片/视频的绘制
在canvas中,我们可以通过 drawImage() 的方法来绘制图片或视频文件,其语法为:
ctx.drawImage( img, clip_x, clip_y, clip_w, clip_h, x, y, width, height );
其中红色的参数为可选项,它们的含义如下:
⑴ 我们先来看下最简单的形式 ctx.drawImage(img, x, y):
<canvas id="myCanvas" width="300" height="300" style="border:solid 1px #CCC;"> 您的浏览器不支持canvas,建议使用最新版的Chrome </canvas> <script> var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,30,30); //在画布坐标(30,30)的位置绘制图片 } </script>
注意如同我们在第一章说讲到的,应当等图片onload之后才执行绘图代码,防止代码在图片加载到之前就执行。效果如下:
⑵ 我们也可以通过添加 width 和 height 参数来缩放图片:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,30,30,250,150); //在画布坐标(30,30)的位置绘制一张宽度为250,高度为150的图片 }
⑶ 我们把裁剪图片的参数 clip_x, clip_y, clip_w, clip_h 也都加上:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,10,20,300,300,30,30,250,150); //在画布坐标(30,30)的位置绘制一张宽度为250、高度150的图片,这种图片是在img上坐标为(10,20)的位置所裁剪出来的宽高均为300的区域 }
注意这里被拉伸的图片已经不再是一开始的那张原始图了,而是原始图在其坐标(10,20)处开始裁剪到的宽高均为300的区域,也就是把这个裁剪到的区域,再伸缩为宽250、高150。
把参数全部用上虽然感觉有点繁琐,但它可以实现像css sprite的效果,从而有效减少图片文件请求数量,进而减少我们要写img.onload=.... 的次数。
说到裁剪我们顺便说说另一个canvas方法 clip() ,它是更地道的“裁剪”方法,在使用它之前需要绘制一个闭合路径(比如一个rect),使用clip()之后的绘制语句所绘制的对象只能显示被裁剪的区域(就一开始定义的那个闭合路径里的区域,类似PS的蒙板、Flash里的遮罩层):
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); ctx.rect(60,60,100,100); //绘制裁剪区域(一个矩形) ctx.clip(); //设置上一个闭合路径为裁剪蒙板 var img = new Image(); img.src = "http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg"; img.onload = function(){ ctx.drawImage(img,10,20); }
我们说回一开始讲的 drawImage() 方法,它有一个蛮屌的功能——获取和绘制视频当前图像,这里提供下3wschool的案例。
利用这个功能,再配合ImageData对象的方法,我们甚至可以用来替换绿屏视频的绿色背景。至于什么是ImageData对象,这是我们接下来要讲的地方。
ImageData你可以理解为“含像素数据的图形对象”,“像素数据”指的是该图形对象上的每一个有序的像素的数据,每个像素都有它对应的颜色数据(RGBA值)。
我们可以通过 createImageData(width,height) 方法来创建一个ImageData对象,然后通过 putImageData(imgData,x,y) 方法把ImageData对象放到画布上:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); //创建一个宽为200,高为100的ImageData对象 ctx.putImageData(imgData,50,60); //将上述创建的ImageData对象放到画布坐标(50,60)的位置
运行上述代码,不会有任何图形显示出来,因为我们仅仅创建了一个没有任何数据的ImageData对象。如何给ImageData对象赋予像素数据、定义其每一点像素的颜色呢?处理这问题要用到ImageData对象的 .data 属性。
我们要知道,一个图形对象上的每一点像素都是从上到下一行一行(每一行里又是从左到右)有序地排列着的,而每一个像素又有四个数值(RGBA)表示它的颜色。
比如下方有一个非常简单的图形对象(假设我把它放大了75倍,方便查看),它一共只有四个像素点,这四个像素点的RGBA数值分别是(255,255,0,255)、(0,255,64,255)、(43,149,255,255)、(236,103,100,51) :
那么这个图形对象的“像素数据”可以看为一个数组: [255,255,0,255,0,255,64,255,43,149,255,255,236,103,100,51]
也就是把四个像素的RGBA数据依次拼起来。当然这里只是一个非常简单的例子,常规的图像可能有几千几万个像素,但它们的像素数据都遵循这种存储方式。
而ImageData对象的 .data 属性正是返回这么一个存储像素数据的数组(没错就是数组,故有length属性)。我们可以这样进一步完善上方的代码:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0;i<imgData.data.length;i+=4) //遍历ImageData对象的每一个像素点,并给它们上色 { imgData.data[i+0]=255; imgData.data[i+1]=100; imgData.data[i+2]=0; imgData.data[i+3]=255; } ctx.putImageData(imgData,50,60);
此处我们给该ImageData对象的每一个像素都赋值了RGBA(255,100,0,255)的颜色,执行效果如下:
我们试着不要绘制纯色的ImageData对象,来个多彩的:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0,t=255;i<imgData.data.length;i+=4) //遍历ImageData对象的每一个像素点,并给它们上色 { if(t<=0) t=255; imgData.data[i+0]=255-t; imgData.data[i+1]=t; imgData.data[i+2]=255-t; imgData.data[i+3]=255; --t; } ctx.putImageData(imgData,50,60);
效果如下:
其实 putImageData() 方法还有四个可选参数,可以用来裁剪ImageData对象上的指定区域。其全部参数为:
ctx.putImageData( imgData, x, y, clip_X, clip_Y, clip_Width, clip_Height);
clip_X,clip_Y分别表示相对于ImageData对象的裁剪起始点坐标,clip_Width, clip_Height表示要裁剪的矩形区域宽高。例如上面的例子我们可以稍微裁剪一下,裁剪成正方形吧:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0,t=255;i<imgData.data.length;i+=4) { if(t<=0) t=255; imgData.data[i+0]=255-t; imgData.data[i+1]=t; imgData.data[i+2]=255-t; imgData.data[i+3]=255; --t; } ctx.putImageData(imgData,60,60,50,0,100,100); //裁剪imgData上坐标为(50,0)且宽高均为100px的矩形区域,并在画布(60,60)的坐标上画出来
每一个ImageData对象都有其 width 和 height 属性,对应其宽度和高度:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0;i<imgData.data.length;i+=4) { imgData.data[i+0]=255; imgData.data[i+1]=100; imgData.data[i+2]=255; imgData.data[i+3]=255; } ctx.putImageData(imgData,50,60); console.log("宽度是" + imgData.width + ",高度是" + imgData.height ); //输出其宽高
另外介绍下获取已有ImageData对象的两个方式,首先是直接用 createImageData( imgData ) 的方式来获取已有的ImageData对象的尺寸,注意这里只会获取其尺寸,不会把已有对象的像素数据也复制了:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var imgData=ctx.createImageData(200,100); for (var i=0;i<imgData.data.length;i+=4) { imgData.data[i+0]=255; imgData.data[i+1]=100; imgData.data[i+2]=255; imgData.data[i+3]=255; } ctx.putImageData(imgData,50,10); var imgData2=ctx.createImageData(imgData); //新建一个尺寸与已有的imgData一致的新ImageData对象imgData2,注意是不会复制其像素数据的 for (var i=0;i<imgData2.data.length;i+=4) //给imgData2上色 { imgData2.data[i+0]=155; imgData2.data[i+1]=200; imgData2.data[i+2]=155; imgData2.data[i+3]=155; } ctx.putImageData(imgData2,50,160);
另一种方法才算是地道的获取、复制已有ImageData对象的方法,即 getImageData() 方法,该方法返回一个 ImageData 对象,此对象拷贝了画布指定矩形区域的像素数据,其语法如下:
var newImgData=ctx.getImageData( x, y, width, height );
其中参数 x,y 分别表示要从画布上开始复制的起始点坐标,width,height 分别表示要复制矩形区域的宽度和高度。我们来个示例:
<body> <img id="img" src="http://images.cnblogs.com/cnblogs_com/vajoy/558869/o_avatar.jpg" /> <canvas id="myCanvas" width="300" height="600" style="border:solid 1px #CCC;"> 您的浏览器不支持canvas,建议使用最新版的Chrome </canvas> <script> var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = document.getElementById("img"); img.onload = function(){ ctx.drawImage(img,10,10); var imgData=ctx.getImageData(0,0,c.width,c.height); ctx.putImageData(imgData,0,300); } </script> </body>
执行上述代码时发现
var imgData=ctx.getImageData(0,0,c.width,c.height);
ctx.putImageData(imgData,0,300);
这两句没有起任何作用,且Chrome报错“Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.”:
这是何处导致的问题?我们的代码写错或写漏了什么么?或者图片不能作为ImageData对象来获取?
其实不然,我们的代码没有特别的错误,而图片或者视频文件都可以算作ImageData对象,之所以会发送这个错误,是因为我们在canvas上放置了一张跨域的图片。
一旦canvas发现你绘制了一张跨域的图片时,它就会认为此时的画布是"tainted"、被污染的,从而不允许你操作该图片的像素,从而防止多种类型的XSS/CSRF攻击。
对于此问题的详细描述可以查看这里,而解决此问题的办法是在服务器的环境下来运行代码(当然图片也要放到项目目录下作为本地文件)。
我们使用tomcat/IIS/wamp等服务器来运行我们的项目,便可成功执行、得到我们想要的效果:
在上面代码的基础上,我们可以来执行一个有趣的效果,它类似于制图软件中将一张图片颜色“取反”,也就是说假如图片上某一点像素颜色是RGBA(255,0,100,255),取反后该像素的RGBA变为(0,255,155,255)。注意透明度Alpha是保持原值的。我们可以这样写代码:
var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); var img = document.getElementById("img"); img.onload = function(){ ctx.drawImage(img,10,10); var imgData=ctx.getImageData(0,0,c.width,c.height); for (i=0; i<imgData.width*imgData.height*4;i+=4) { imgData.data[i]=255-imgData.data[i]; imgData.data[i+1]=255-imgData.data[i+1]; imgData.data[i+2]=255-imgData.data[i+2]; imgData.data[i+3]=255; } ctx.putImageData(imgData,0,300); }
效果如下:
关于图像对象操作的介绍我们就讲到这,最后来个有趣也实用的应用。还记得我们在前面提到的,可以利用 drawImage 和 ImageData 的方法来替换屏幕的绿色背景么?
在MSDN有这么一段介绍:
从两个视频中读写像素到另一个视频中所需的代码要求使用两个视频、两个画布和一个最终画布。一次捕捉视频上的一帧,然后绘制到两个单独的画布上。这样允许读回数据。
其中画布1和画布2分别用来绘制两个视频当前帧的画面(注意这俩个画布我们设其样式visibility:hidden,即不可见):
ctxSource1.drawImage(video1, 0, 0, videoWidth, videoHeight);
ctxSource2.drawImage(video2, 0, 0, videoWidth, videoHeight);
然后我们可以轻松从这两个画布已绘制出来的图像并转为ImageData对象:
currentFrameSource1 = ctxSource1.getImageData(0, 0, videoWidth, videoHeight);
currentFrameSource2 = ctxSource2.getImageData(0, 0, videoWidth, videoHeight);
最后从浏览绿屏的像素数组中搜索绿色像素,如果找到,代码将用背景场景中的像素替换所有绿色像素:
for (var i = 0; i < n; i++) { // Grab the RBG for each pixel: r = currentFrameSource1.data[i * 4 + 0]; g = currentFrameSource1.data[i * 4 + 1]; b = currentFrameSource1.data[i * 4 + 2]; // If this seems like a green pixel replace it: if ( (r >= 0 && r <= 59) && (g >= 74 && g <= 144) && (b >= 0 && b <= 56) ) // Target green is (24, 109, 21), so look around those values. { pixelIndex = i * 4; currentFrameSource1.data[pixelIndex] = currentFrameSource2.data[pixelIndex]; currentFrameSource1.data[pixelIndex + 1] = currentFrameSource2.data[pixelIndex + 1]; currentFrameSource1.data[pixelIndex + 2] = currentFrameSource2.data[pixelIndex + 2]; currentFrameSource1.data[pixelIndex + 3] = currentFrameSource2.data[pixelIndex + 3]; } }
MSDN还专门提供了一个实例(点我查看),查看该页面源码即可获得全部代码,有兴趣的朋友可以研究下。
本章就讲到这里,下一章将介绍canvas常用的变形转换功能,共勉~