今天要介绍的是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之后才执行绘图代码,防止代码在图片加载到之前就执行。效果如下:

⑵ 我们也可以通过添加 widthheight 参数来缩放图片:

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对象都有其 widthheight 属性,对应其宽度和高度:

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常用的变形转换功能,共勉~

HTML5- Canvas入门(五)的更多相关文章

  1. HTML5 canvas入门

    HTML5 Canvas入门 <canvas> 标签定义图形,比如图表和其他图像,您必须使用脚本来绘制图形.在画布上(Canvas)画一个红色矩形,渐变矩形,彩色矩形,和一些彩色的文字. ...

  2. html5 canvas 笔记五(合成与裁剪)

    组合 Compositing globalCompositeOperation syntax: globalCompositeOperation = type 注意:下面所有例子中,蓝色方块是先绘制的 ...

  3. html5 Canvas绘制图形入门详解

    html5,这个应该就不需要多作介绍了,只要是开发人员应该都不会陌生.html5是「新兴」的网页技术标准,目前,除IE8及其以下版本的IE浏览器之外,几乎所有主流浏览器(FireFox.Chrome. ...

  4. HTML5 canvas绘制线条曲线

    HTML5 canvas入门 线条例子 1.简单线条 2.三角形 3.填充三角形背景颜色 4.线条颜色以及线条大小 5.二次贝塞尔曲线 6.三次贝塞尔曲线 <!doctype html> ...

  5. Canvas入门笔记-实现极简画笔

    今天学习了Html5 Canvas入门,已经有大神写得很详细了http://www.cnblogs.com/tim-li/archive/2012/08/06/2580252.html#8 在学习过后 ...

  6. HTML5 Canvas 画图入门

    HTML5 Canvas 画图入门 HTML5 Canvas 画图入门,仅供学习參考 <!DOCTYPE html> <html> <head> <meta ...

  7. HTML5简单入门系列(五)

    前言 本篇将讲述HTML5的服务器发送事件(server-sent event) Server-Sent 事件 Server-Sent 事件是单向消息传递,指的是网页自动获取来自服务器的更新. 以前的 ...

  8. 06. Web大前端时代之:HTML5+CSS3入门系列~HTML5 画布

    Web大前端时代之:HTML5+CSS3入门系列:http://www.cnblogs.com/dunitian/p/5121725.html 我们先看看画布的魅力: 初始画布 canvas默认是宽3 ...

  9. html5 基础入门

    html5 基础入门 前言介绍 HTML5草案的前身名为 Web Applications 1.0,于2004年被WHATWG提出,于2007年被W3C接纳,并成立了新的 HTML工作团队. 如果从狭 ...

  10. 《html5 从入门到精通》读书笔记(一)

    今天看了<html5 从入门到精通>这本书,感觉阅读下来很舒心,不像阅读其他书籍很揪心.html增加的知识点,我觉得非常有价值,看完几章记录了一些内容,不但能巩固,也为下次遗忘知识点做好准 ...

随机推荐

  1. 学习AOP之认识一下Spring AOP

    心碎之事 要说知道AOP这个词倒是很久很久以前了,但是直到今天我也不敢说非常的理解它,其中的各种概念即抽象又太拗口. 在几次面试中都被问及AOP,但是真的没有答上来,或者都在面上,这给面试官的感觉就是 ...

  2. Android笔记——Button点击事件几种写法

    Button点击事件:大概可以分为以下几种: 匿名内部类 定义内部类,实现OnClickListener接口 定义的构造方法 用Activity实现OnClickListener接口 指定Button ...

  3. JavaScript常见的五种数组去重的方式

    ▓▓▓▓▓▓ 大致介绍 JavaScript的数组去重问题在许多面试中都会遇到,现在做个总结 先来建立一个数组 var arr = [1,2,3,3,2,'我','我',34,'我的',NaN,NaN ...

  4. [C#] C# 知识回顾 - 异常介绍

    异常介绍 我们平时在写程序时,无意中(或技术不够),而导致程序运行时出现意外(或异常),对于这个问题, C# 有专门的异常处理程序. 异常处理所涉及到的关键字有 try.catch 和 finally ...

  5. MAC Osx PHP安装指导

    php.ini的位置 Mac OS X中没有默认的php.ini文件,但是有对应的模版文件php.ini.default,位于/private/etc/php.ini.default 或者说 /etc ...

  6. Angular源码分析之$compile

    @(Angular) $compile,在Angular中即"编译"服务,它涉及到Angular应用的"编译"和"链接"两个阶段,根据从DO ...

  7. C++随笔:从Hello World 探秘CoreCLR的内部(1)

    紧接着上次的问题,上次的问题其实很简单,就是HelloWorld.exe运行失败,而本文的目的,就是成功调试HelloWorld这个控制台应用程序. 通过我的寻找,其实是一个名为TryRun的文件出了 ...

  8. 【夯实PHP基础】UML序列图总结

    原文地址 序列图主要用于展示对象之间交互的顺序. 序列图将交互关系表示为一个二维图.纵向是时间轴,时间沿竖线向下延伸.横向轴代表了在协作中各独立对象的类元角色.类元角色用生命线表示.当对象存在时,角色 ...

  9. JQuery阻止事件冒泡

    冒泡事件就是点击子节点,会向上触发父节点,祖先节点的点击事件. 我们在平时的开发过程中,肯定会遇到在一个div(这个div可以是元素)包裹一个div的情况,但是呢,在这两个div上都添加了事件,如果点 ...

  10. Mono下的WCF的Bug?

    最近一段时间,一直在折腾Mono,折腾Linux.让我无比痛苦的是Mono下的WCF的坑真的是太多了,这不又遇到了一个莫名其妙的问题. 环境:mono 3.2.1,Jexus 5.4.3,OS Cen ...