Canvas教程

  Canvas是HTML5新引入的一个元素,它的英文意思就是画布,引入Canvas画布元素的目的就是为了更加灵活的绘制图形,支持2D,也支持三维WebGL,本节课主要说的是二维绘图, Canvas相比CSS绘制图形有更强的自定义性,形象点说Canvas就像平时常用的设计软件CAD,可以绘制直线、圆弧、样条曲线,通过Javascript可以控制浏览器提供的Canvas API进行绘制。

  为了以最快的速度了解Canvas,先提供一个小的案例,来体验一下Canvas的用途。

入门案例

源码下载

  下面的代码是利用Canvas绘制两条直线。

11   <body>
12   <canvas id="canvas" width="100" height="100"></canvas>
13   <script>
14   //    获取canvas画布元素
15       let canvas=document.getElementById('canvas');
16   //    获取canvas元素上下文
17       let c=canvas.getContext('2d');
18   //    直线起点坐标
19       c.moveTo(0,0);
20   //    直线第2个点坐标
21       c.lineTo(50,50);
22   //    直线第3个点坐标
23       c.lineTo(0,100);
24   //    把点连成直线绘制出来
25       c.stroke();
26   </script>
27   </body> 

代码测试

  尝试更改代码中的点坐标的值刷新浏览器查看直线位置变化体会,二维canvas画布的笛卡尔坐标系与canvas元素的对应关系。

代码解析

  1.第一步毫无疑问先创建一个canvas元素,浏览器默认canvas是一个行内块元素,canvas元素和其他元素一样可以设置宽高度、背景颜色,canvas画布的内容创建和普通元素有所区别,一般通过 Javascript语言操作canvas相关方法来完成,比如使用moveTo()、lineTo()方法创建直线,rect()方法创建矩形......

  2.要想获得canvas元素的直线、矩形等绘制方法,首先要使用DOM对象的方法getElementById获得canvas元素,canvas对象具有方法getContext(),canvas对象执行它的方法getContext(),参数为"2d",可以返回 一个对象,该对象具有一系列方法和属性,利用这些方法和属性可以创建各种各样的二维图形。查看第17行代码可以看出,执行方法getContext()返回的对象赋值给了变量c,然后后面调用moveTo()、lineTo()等方法, 直接使用变量c加符号点(.)即可。

  3.canvas绘制直线的方式,不是使用一个直线绘制方法一步到位,而是先定义一系列点,两点确定一条直线,连点成线。第19行用到的方法moveTo用来确地第一个点,第21、23行代码用到的lineTo()方法用来确定第n个点,这两个方法的 参数是二维坐标(x,y)。默认坐标原点是canvas画布的左上角,x轴正方向水平向右,y轴正方向水平向下。x和y的单位就是像素px。

cavas坐标系

  4.第25行代码stroke()方法的作用,简单形象的描述就是你把点定义好了,它的任务就是连点成线,命令浏览器你给我画出来。 这样描述不专业,但是对于目前没有图形学基础而用过CAD、PS等绘图软件或者学过几何的人,很容易理解。

线条添加样式

  上面代码完成了形状回制,但是没有定义线条的颜色、粗细等样式,浏览器渲染的效果是默认的直线样式,canvas和CSS一样可以给对象添加样式属性,只不过CSS针对的是元素对象,canvas的样式属性针对的是几何图形对象。

let canvas=document.getElementById('canvas');
let c=canvas.getContext('2d');
//定义线条宽度
c.lineWidth=10;
//定义线条颜色
c.strokeStyle="#0000FF";
//定义透明度
c.globalAlpha=0.5;
c.moveTo(0,0);
c.lineTo(50,50);
c.lineTo(0,100);
c.stroke();
样式属性 作用
lineWidth 设置线宽 数字,默认1,单位px,
strokeStyle 设置线条颜色 比如"#0000FF"、"blue"、"#rga(0,0,1)"三种方式都表示蓝色
globalAlpha 透明度,会与背景颜色融合 区间[0,1]

矩形线条rect()

  语法:rect(x,y,width,height)

let canvas=document.getElementById('canvas');
let c=canvas.getContext('2d');
//绘制矩形线框,矩形左上角坐标(10,20),width宽80,height高50
c.rect(10,20,80,50);
c.stroke();

圆弧线arc()

  语法:arc(圆心横坐标x,圆心纵坐标y,半径r,起始角, 结束角,false/true);

//完整圆,圆心坐标 (50,50),半径30
      c.arc(50,50,30,0,Math.PI*2);

//四分之三圆弧,true表示顺时针
      c.arc(50,50,30,0,Math.PI*1.5,false);

//四分之一圆弧,false表示逆时针
      c.arc(50,50,30,0,Math.PI*1.5,true);
canvas圆弧角度

Canvas绘制正六边形

源码下载

  正六边形拆分来看,也就是六条直线,只要计算出正六边形六个点的坐标连点成线就可以,你可以手动计算,也可以利用Javascript程序计算。

cavas正六边形
12       let canvas=document.getElementById('canvas');
13       c=canvas.getContext('2d');
14   //    把坐标原点平移到canvas画布的中心点
15       c.translate(400,300);
16   //    设置边线颜色
17       c.strokeStyle = "#158539";
18   
19   //    求解正六边形6个点的纵横坐标
20       let x = [];//横坐标数组
21       let y = [];//纵坐标数组
22   //    循环计算六个点的坐标
23       for( let i=0;i<6;i++){
24           let r = 200;//正六边形外接圆半径
25           x[i]=r*Math.cos(2*Math.PI/6*i);//计算横坐标,并存入x数组[]
26           y[i]=r*Math.sin(2*Math.PI/6*i);//计算纵坐标,并存入y数组[]
27       }
28   
29   //    调用数组里的数据定义点的位置
30       c.moveTo(x[0],y[0]);//定义初始点
31   //    利用for循环定义第2~6个点
32       for(let i = 1;i<6;i++){
33           c.lineTo(x[i],y[i]);
34       }
35       c.closePath();//设置轮廓闭合,最后一个点与第一个点连线
36       c.stroke();//开始绘制线条轮廓

代码测试

  1.更改第15行代码translate()方法的参数,观察图形是否平移。

  2.更改第24行代码半径值,观察六边形尺寸变化。

  3.在第25、26行代码三角函数的角度值同时增加某个值,比如+Math.PI/2,观察六边形旋转变化。

代码解析

  1.为了更方便把几何图形绘制在canvas画布中间,需要移动canvas画布笛卡尔坐标系的坐标原点,具体定义见代码第15行c.translate(400,300);。translate作用就是重新定义坐标系原点(0,0) 相对canvas画布的位置,参数400和300是canvas画布宽高度的一半,平移后的坐标系见上图。

  2.利用Javascript程序求解正六边形六个顶点的纵横坐标,这和canvas关系不大,主要是Javascript语言和数学几何图形算法的应用,for循环程序结构里用到了Javascript内置对象Math的相关知识 《Math对象》。正六边形的六个点全部在一个圆上,把一个圆均分六个点,得到六个相位角,求解相位角对应的正弦余弦值再乘以正六边形外接圆 半径结果对应的就是6各顶点的纵横坐标,把求解结果纵横坐标分别存入2个数组,供后面调用。

  3.利用求解得到的顶点坐标,和上面一样,对于有规律的多使用for循环语句,第32行到第34行代码的作用就是循环执行lineTo()方法,来设置第2~6个顶点的位置。

  4.6个顶点相连确定的是5条直线,也就是说正六边形不能闭合,往往几何图形会是闭合形状的,所以W3c组织为canvas引入一个closePath()闭合轮廓曲线方法,该方法表示的意思就是当c.stroke();该语句开始执行 告诉浏览器绘制渲染效果图的时候,把lineTo()方法定义的最后一个顶点与moveTo()方法定义的第一个顶点连出来一条直线,把开环曲线曲线变成闭环曲线。

Canvas绘制太极

源码下载
canvas绘制太极

  下面的代码主要是使用arc绘制图形,相比上一节课又引入了与strokeStyle对应fillStyle定义颜色的属性,与stroke()对应fill()对应渲染绘制的方法。

6    <body>
7    <div style="width: 800px;margin: auto">
8        <!--定义画布-->
9        <canvas id="canvas" width="800px" height="600px" style="background: #ddeeff">
10       </canvas>
11   </div>
12   <script type="text/javascript">
13       let canvas=document.getElementById('canvas');
14       c=canvas.getContext('2d');
15   //    把坐标原点平移到canvas画布的中心点
16       c.translate(400,300);
17   
18   //    阳鱼体
19       c.fillStyle="white";//预定义填充颜色
20       c.arc(0,0,200,Math.PI/2,Math.PI*1.5);//阳鱼体外圆弧
21       c.arc(0,-100,100,Math.PI*1.5,Math.PI/2);//S型上半部分
22       c.arc(0,100,100,Math.PI*1.5,Math.PI/2,true);//S型下半部分
23       c.fill();//渲染绘制外轮廓
24   
25   //    阴鱼体
26       c.beginPath();//绘制新的外轮廓,控制fill()的作用域
27       c.fillStyle="black";
28       c.arc(0,0,200,Math.PI/2,Math.PI*1.5,true);//阴鱼体外圆弧
29       c.arc(0,-100,100,Math.PI*1.5,Math.PI/2);//S型上半部分
30       c.arc(0,100,100,Math.PI*1.5,Math.PI/2,true);//S型下半部分
31       c.fill();//渲染绘制外轮廓
32   
33   //    阳鱼眼
34       c.beginPath();
35       c.fillStyle="white";
36       c.arc(0,100,30,0,Math.PI*2);//阳鱼眼圆弧
37       c.fill();
38   
39   //    阴鱼眼
40       c.beginPath();
41       c.fillStyle="black";
42       c.arc(0,-100,30,0,Math.PI*2);//阴鱼眼圆弧
43       c.fill();
44   </script>
45   </body>

代码测试

  1.把方法fill()更改为stroke()测试浏览器网页渲染效果。

  2.把属性fillStyle更改为strokeStyle测试浏览器网页渲染效果。

代码解析

  1.每一节的课程案例都会在上一节课案例的基础上引入新的的知识点,建议按按照顺序学习,基础好的话,可以只看代码,不用看文字和视频。上面的代码只需要解释第19到第26行代码,其它都与此相似。

  2.第20~21三行代码就是利用圆弧方法arc()绘制一个由三段半圆弧拼接成的封闭外轮廓,也就是太极左侧鱼体轮廓,一个大的外轮廓半圆,和两个半圆构成的S形状,绘制的时候要注意三段圆弧线首尾相接。 你可以看到第20行代码起始角度是π/2,结束角度是3π/2,绘制方向是顺时针,arc()方法最后一个表示顺时针参数的关键字false省略掉了。 起始角度和结束角度不分大小,第21行代码相比第20行,起始角度是3π/2,结束角度是π/2,默认顺时针,绘制弧线的时候从3π/2开始沿着顺时针方向绘制到π/2角度。 第22行代码绘制的是S型的下半部分,他定义的参数最后一个是true,表示逆时针绘制圆弧。现在你可以在纸上把上面三个圆弧的绘制过程重复一遍验证是否一气呵成,首尾相接。 圆弧默认的绘制方向是顺时针绘制,从起始角度开始回制到结束角度绘制完成,当你调用stroke()方法的时候,如果两条圆弧线之间连接的时候,不是一个圆弧的起始点与领一个圆弧的结束点相连接,浏览器渲染程序 就会从一个圆弧的结束点拉一条直线引到领一个圆弧的开始点,两条圆弧首尾相接的时候,也会这样,但是首尾之间的距离是0,你也就看不到多余的直线。

  3.调用方法fill()和调用方法stroke()一样都是告诉浏览器渲染出定义的几何图形,只是方式不同,stroke()是描点成线,fill()是填充一个封闭的外轮廓,如果外轮廓不封闭,fill方法会利用直线把它封闭, 封闭的规则是,比如他会把lineTo定义的最后一个点与moveTo定义的第一个点相连,把一个开环多段线变成闭合状态。调用方法stroke()绘制外轮廓线条的时候往往使用属性strokeStyle定义线条颜色,对于方法 fill()也一样,不过调用的是fillStyle属性来定义封闭轮廓的填充颜色,看属性和方法的名字都可以分辨出哪个属性属于那个方法。

  4.第26行代码中的方法beginPath(),相当于告诉浏览器beginPath()前面的轮廓与后面的轮廓是独立的,fill()方法每次填充一个独立的外轮廓,也就是说负责的图形中beginPath()方法往往与方法fill()或stroke() 结伴出现。fill()填充的是beginPath()和fill()之间代码定义的轮廓,stroke()连接的是beginPath()和stroke()之间定义的点或圆弧。

Canvas绘制操场跑道形状

源码下载

  绘制之前先分析图形,跑道的形状是两段半圆弧和两条直线组成,只要分别绘制出来连在一起就行。

cavas跑道形状

  绘制方式1

13   //    方法一
14       let canvas=document.getElementById('canvas');
15       c=canvas.getContext('2d');
16   //    坐标原点居中
17       c.translate(400,300);
18   //    预定义全部线条样式
19       c.lineWidth=40;
20       c.strokeStyle="#FF0000";
21   //    绘制第1条直线
22       c.moveTo(-200,100);
23       c.lineTo(200,100);
24       c.stroke();
25   //    绘制第2条直线
26   //    使用moveTo的时候不需要用beginPath(),moveTo本身就是起点的意思
27       c.moveTo(-200,-100);
28       c.lineTo(200,-100);
29       c.stroke();
30   //    左半圆弧
31       c.beginPath();//单独的轮廓线
32       c.arc(-200,0,100,Math.PI*1.5,Math.PI/2,true);
33       c.stroke();//渲染填充轮廓
34   //    右半圆弧
35       c.beginPath();
36       c.arc(200,0,100,Math.PI*1.5,Math.PI/2);
37       c.stroke();//渲染填充轮廓

  绘制方式2

39   //    方法二
40       let canvas=document.getElementById('canvas');
41       c=canvas.getContext('2d');
42       c.translate(400,300);
43       c.lineWidth=40;
44       c.strokeStyle="#FF0000";
45       c.arc(-200,0,100,Math.PI*1.5,Math.PI/2,true);//左半圆弧
46       c.arc(200,0,100,Math.PI/2,Math.PI*1.5,true);//右半圆弧
47       c.closePath();//闭合轮廓
48       c.stroke();//渲染填充轮廓

代码解析

  1.方式1不再解释,注释里面已经作了简单说明,都是前面学习过的知识点。

  2.方式2绘制了两条圆弧,调用方法stroke()时候,两个圆弧的首尾会用直线相连,也就是第一个圆弧的结束点会去连接第二个圆弧的开始点,但是第二个圆弧的的结束点不会去连接第一个圆弧的起点, 调用方法closePath()就可以定义第二个圆弧的结束点区连接第一个圆弧的起始点,从这里你也可以发现,第一个圆弧线的起点类似绘制多段线的时候moveTo定义定义的起点,每一个圆弧的起始点、结束点类似 lineTo()定义的第2~n个点

拓展(绘制一个扇形)

51       let canvas=document.getElementById('canvas');
52       c=canvas.getContext('2d');
53       c.translate(400,300);
54       c.lineWidth = 4;//外轮廓线条宽度
55       c.strokeStyle="black";//外轮廓线条颜色
56       c.fillStyle="#FF0000";//填充颜色
57       c.arc(0,0,150,4*Math.PI/3,5*Math.PI/3);
58       c.arc(0,0,100,5*Math.PI/3,4*Math.PI/3,true);
59       c.closePath();
60       c.stroke();//绘制外轮廓
61       c.fill();//填充外轮廓

Canvas绘制五角星

源码下载

  绘制五角星的思路和正六边形类似,首先等分一个圆,求出圆弧线上的五个等距点的坐标值,然后连线绘制五角星,就像平时手绘一样,连接两点的时候跳过中间的点。

cavas绘制五角星
<body>
6    <div style="width: 800px;margin: auto">
7        <!--定义画布-->
8        <canvas id="canvas" width="800px" height="600px" style="background: #ddeeff">
9        </canvas>
10   </div>
11   <script type="text/javascript">
12       var canvas=document.getElementById('canvas');
13       c=canvas.getContext('2d');
14       c.translate(400,300);
15       c.fillStyle = "#FF0000";
16       var x = [];//顶点横坐标数组
17       var y = [];//顶点纵坐标数组
18   //    在圆上求解出五角星5各顶点的纵横坐标
19       for( var i=0;i<5;i++){
20           var r = 200;//5个顶点分布半径
21           x[i]=r*Math.cos(2*Math.PI/5*i);
22           y[i]=r*Math.sin(2*Math.PI/5*i);
23       }
24   //    定义点的连接顺序,两点之间跳过中间一个点(0—2—4—1—3)
25       c.moveTo(x[0],y[0]);
26       c.lineTo(x[2],y[2]);
27       c.lineTo(x[4],y[4]);
28       c.lineTo(x[1],y[1]);
29       c.lineTo(x[3],y[3]);
30       c.closePath();
31       c.stroke();
32       c.fill();
33   </script>
34   </body>

代码测试

  删除第32行fill()填充代码,查看五角星的线条轮廓

canvas太极旋转动画

源码下载

  在《canvas绘制太极》代码的基础上进行更改,主要利用与translate方法一样都是属于变换的rotate旋转变换,你可以理解为rotate旋转了坐标系,几何图形随着坐标一起旋转。

6    <body>
7    <div style="width: 800px;margin: auto">
8        <!--定义画布-->
9        <canvas id="canvas" width="800px" height="600px" style="background: #ddeeff">
10       </canvas>
11   </div>
12   <script type="text/javascript">
13       let canvas=document.getElementById('canvas');
14       c=canvas.getContext('2d');
15       c.translate(400,300);
16   //    获取当前时间赋值给全局变量T0
17       let T0 = new Date();
18       function fun() {
19   //    获得每次调用fun函数的时间
20           let T1 = new Date();
21   //    求解requestAnimationFrame两次调用函数fun的时间差
22           let t = T1-T0;
23   //    把第n次的调用时间赋值给第n-1次调用时间
24           T0 = T1;
25   //    rotate旋转变换几何图形
26           c.rotate(t*Math.PI/3600);
27   //循环调用函数fun(),时间间隔不稳定,一般60HZ/s
28           requestAnimationFrame(fun);
29   
30   //    太极轮廓,参考《canvas绘制太极》,代码没有变化
31           c.fillStyle="white";
32           c.arc(0,0,200,Math.PI/2,Math.PI*1.5);
33           c.arc(0,-100,100,Math.PI*1.5,Math.PI/2);
34           c.arc(0,100,100,Math.PI*1.5,Math.PI/2,true);
35           c.fill();
36           c.beginPath();
37           c.fillStyle="black";
38           c.arc(0,0,200,Math.PI/2,Math.PI*1.5,true);
39           c.arc(0,-100,100,Math.PI*1.5,Math.PI/2);
40           c.arc(0,100,100,Math.PI*1.5,Math.PI/2,true);
41           c.fill();
42           c.beginPath();
43           c.fillStyle="white";
44           c.arc(0,100,30,0,Math.PI*2);
45           c.fill();
46           c.beginPath();
47           c.fillStyle="black";
48           c.arc(0,-100,30,0,Math.PI*2);
49           c.fill();
50       }
51       fun();//调用函数fun()
52   </script>
53   </body>

代码测试

  更改代码第26行函数的参数,把3600行改为360,观察太极的旋转速度变化。

执行流程

  整个代码就是利用第28行代码的requestAnimationFrame方法循环调用函数fun,每次调用函数,执行第26行代码c.rotate(t*Math.PI/3600);的时候,坐标系都会旋转t*Math.PI/3600弧度, 第31行到第49行太极的坐标虽然没变化,但是坐标系变了,自然太极相对画布发生了旋转,重复调用fun函数,太极的相位角度逐渐变化,人眼就会产生太极旋转的动画视觉效果。

代码解析

  第26行代码表示按照一定的角度旋转当前图形,方法参数是弧度,函数每调用一次,几何图形角度就变化一次。 canvas和CSS一样拥有平移、旋转、缩放等变换方法,translate()可以重新定义坐标原点位置,也可以用于平移动画,rotate()方法常常用于旋转几何图形, 连续更换角度,自然就可以产生动画效果。

变换方法 参数 作用
translate() 比如(10,20)表示x、y方向分别正向平移10px、20px 重新定义坐标原点(0,0)位置
rotate() 弧度 图形旋转变换
scale() 比如scale(0.5,0.8)表示x、y方向分别缩放0.5、0.8倍 图形整体缩放

  第28行代码中requestAnimationFrame()是浏览器内置支持的方法,属于window对象,window是浏览器顶级对象,调用该方法的时候可以省略window不写,requestAnimationFrame(fun);语句 执行后会向浏览器发出调用函数fun的请求,但是fun()不是立即被调用执行,而是等浏览器执行完本次fun函数或其它任务空闲下来,requestAnimationFrame发出请求的频率一般不超出每秒60次。 requestAnimationFrame请求频率一方面有浏览器设置的上限,另一方面也会受到程序尤其是大的程序执行时间限制。 如果执行Javascript语句耽误过长时间,可能会低于这个频率,程序语言每一条语句都会占用执行时间,尤其动画更要建立毫秒ms级时间概念,有兴趣可以研究程序执行时间问题。 如果你不太清楚requestAnimationFrame()它的用法,这里你只需要知道它的整体作用就是循环调用一个函数。 为了使图形旋转的均匀,第26行代码rotate()方法的参数,也就是每次执行fun()函数时单次旋转角度, 始终保持和两次调用fun函数的时间相减差值正相关,间隔时间长,单次旋转角度大,间隔时间短,单次旋转角度小,这样的话rotate(t*Math.PI/3600)方法的参数时间差值t的系数Math.PI/3600就是角速度,时间t的单位是ms, 所示角速度的单位是弧度/ms,从毫秒转变为秒是π/3.6弧度/s,也就是50度/s。两次调用函数fun的时间间隔利用第17~24行的代码实现,把上次调用时间作为全局变量声明, 每次调用函数fun的时候,通过第24行代码改变全局变量的值,也就是把本次调用fun的时刻赋值给T0。

canvas插入图片drawImage()

源码下载

  首先利用HTML图片标签img引入图片创建一个图片元素,然后在canvas代码中利用DOM方法获得img元素,然后作为drawImage方法的第一个参数,就可以实现把图片引入canvas画布。 drawImage()方法的后两个参数是坐标(x,y),来定义引入的图片左上角初始位置。引入img元素的目的是为了把它显示在canvas画布中,画布之外不需要显示,这时候可以给img元素引入属性 display: none,前面学习过,block表示块元素,inline-block表示行内块元素,none表示不显示。如果你深入思考就可以知道img元素的功能就是读取解析png、jpg等格式数据的RGBA值,然后利用浏览器 渲染出来,canvas画布插入图片的实现就依赖于HTML的标签img,通过drawImage()方法把img获得的图片RGBA值系列数据传递给canvas画布。

代码测试

  把第19行更改为c.drawImage(panda,400,400);,查看图片位置变化

6    <body>
7    <div style="width: 1200px;margin: auto">
8        <!--引入图片panda.png,同时定义属性display: none隐藏图片-->
9        <img src="panda.png" id="panda" style="display: none">
10       <canvas id="canvas" width="1200px" height="1000px" style="background: #bbddff">
11       </canvas>
12   </div>
13   <script type="text/javascript">
14       let canvas=document.getElementById('canvas');
15       c=canvas.getContext('2d');
16   //    获取图片元素
17       let panda=document.getElementById('panda');
18   //    图片对象作为pandadrawImage方法第一个参数,图片左上角坐标(0,0)
19       c.drawImage(panda,0,0);
20   </script>
21   </body>

Image()创建图片对象

  Image()是浏览器内置支持的构造函数,通过new运算符可以创建一个Image对象,利用Image()创建图片对象和上面先利用img标签引入图片,再使用DOM方法选中是一样的。 好处就是不需要,在写img标签,全部代码都可以使用Javascript的形式实现。new Image();创建得到的对象具有img元素对象的属性,可以在Javascript代码中使用点(.)符号添加, panda.src = "panda.png";语句就是用来加载图片资源。

6    <body>
7    <div style="width: 1200px;margin: auto">
8        <canvas id="canvas" width="1200px" height="1000px" style="background: #bbddff">
9        </canvas>
10   </div>
11   <script type="text/javascript">
12   //    构造函数法
13           let canvas=document.getElementById('canvas');
14           c=canvas.getContext('2d');
15   //        创建Image图片对象
16           let panda=new Image();
17   //        添加src属性加载图片
18           panda.src = "panda.png";
19   //        注册图像加载事件的响应函数(图片加载完成后,触发函数执行)
20           panda.onload = function () {
21               c.drawImage(panda,0,0);
22           };
23   </script>
24   </body>

createElement创建图片对象

  createElement是浏览器非常常用的用于创建元素对象的一个DOM方法,可以创建所有的元素,自然可以用来创建img图片元素对象,使用下面的语句替换上面第16行代码 let panda=new Image();,其它不用更改,也可以实现canvas插入图片。

let panda=document.createElement("img");

canvas图片添加动画

  可以先学习上一节课的旋转动画,再来看本节课的图片平移动画。用到新知识点的地方只有第31行代码中的clearRect()方法,其它的都是前面canvas知识或者Javascript语言的综合灵活应用。

  代码执行基本流程:整体上来看是requestAnimationFrame()方法循环调用fun函数,循环时间是大约4000ms。每次调用的时候都会利用第36行代码c.translate(t*0.16,t*0.2);重新定义坐标系坐标原点的位置,然后插入利用 第37行代码c.drawImage(panda,0,0);执行插入图片并显示出来,虽然每次插入的坐标都是原点,但是坐标原点每次都发生了变化,连续重新在新的位置插如图片就产生了运动的效果。在这个过程中要注意一个问题,前一次插入的图片 仍然会显示,所以需要利用clearRect()方法保证每次调用函数fun的时候刷新一次canvas画布,该方法有四个参数,前两个表示矩形区域的左上角,后两个参数表示矩形区域的右下角。

23   //        添加平移动画
24           let canvas=document.getElementById('canvas');
25           c=canvas.getContext('2d');
26           let panda=document.getElementById('panda');
27           let T = new Date();
28           let T0 = new Date();
29           function fun() {
30               //擦除画布旧的内容,区域左上角(0,0)到右下角(1200,1000)
31               c.clearRect(0,0,1200,1000);
32               let T1 = new Date();
33               let t = T1-T0;
34               T0 = T1;
35   //           每次调用fun函数,图片x、y方向分别平移t*0.16px、t*0.2px
36               c.translate(t*0.16,t*0.2);
37               c.drawImage(panda,0,0);
38               let T2 = new Date();
39   //            fun重复调用时间超出4000ms不再调用
40               if(T2-T<4000){
41                   requestAnimationFrame(fun);
42               }
43           }
44           fun();

canvas点阵

源码下载 体验测试

  基本思路就是先利用canvas文字创建方法fillText()添加一段文字,然后通过方法getImageData()返回画布区域的RGBA像素值,通过for循环批量修改获取的像素来呈现出点阵的效果。

6    <body>
7    <div style="width: 800px;margin: auto">
8        <canvas id="canvas" width="800px" height="600px" style="background: #ddeeff">
9        </canvas>
10   </div>
11   <script type="text/javascript">
12       let canvas=document.getElementById('canvas');
13       c=canvas.getContext('2d');
14       c.translate(400,300);
15   
16   //    文字样式、位置设置
17       c.fillStyle = "#ff0000";//文本填充颜色
18       c.font = "bold 550px 宋体";//字体样式设置
19       c.textBaseline = "middle";//文本与fillText定义的纵坐标
20       c.textAlign = "center";//文本居中(以fillText定义的横坐标)
21   
22   //    画布插入文字,文字坐标(0,0)
23       c.fillText("道",0,0);
24   
25   //    返回(0, 0, 800, 600)矩形区域像素值
26       let pixel = c.getImageData(0, 0, 800, 600);
27   //      获得像素值RGBA组成的数组
28       let arr = pixel.data;
29   
30   //      获取画布像素值RGBA后,清空画布
31       c.clearRect(-400,-300,800,600);
32   
33   //    沿着字体轨迹循环绘制点阵
34       let d = 6;//每6个像素组成的方块区域作为一个单元
35       let w = 800,h = 600;//获取像素宽高度
36       for(let j = 0; j < h; j += d){ //间隔d行像素
37           for(let i = 0; i < w; i += d){//每行像素间隔d个像素
38               if(arr[j*w*4+i*4] == 255){//判断第j行,第i个像素的像素值R分量是否为红色
39   
40   //                绘制直径为d/2-0.8的圆区域
41                   c.beginPath();
42                   c.fillStyle = "blue";
43                   c.arc(i-400, j-300,d/2-0.8 , 0, 2 * Math.PI);
44                   c.fill();
45               }
46           }
47       }
48   </script>
49   </body>

代码解析

  第16到第23行代码的作用就是在画布上绘制出文本,第16到第20行主要是设置文本的样式、位置等属性。

  第26行代码和第28行代码的作用就是获得画布某个区域范围的像素值RGBA,第26行代码方法方法getImageData()返回一个ImageData对象,ImageData对象有一个data属性值, 第28行代码的作用就是获得ImageData对象的data属性值赋值给arr变量,data的属性值是一个数组,里面存储的是getImageData()的参数选中的矩形区域的RGBA像素值,矩形区域就是 getImageData()参数,前两个参数(0, 0)表示矩形区域左上角,(800, 600)表示矩形区域宽高尺寸,像素值RGBA四个分量的值都是0~255,A的值为0表示透明,255表示不透明,RGB代表红绿蓝三种颜色成分。 一个像素在数组中站4个位置,分别存储R、G、B、A,也就是说数组每间隔四个元素存储一个像素。

  代码中getImageData()方法选择的是整个画布区域,也就是说像素值有800x600个,数组data的元素有800x600x4个。基本思路就是按照一定规律通过for循环选取画布某个位置的像素值,选取的像素原则是 第34行代码设置的参数6,间隔6列,间隔6行,每一列是一个for循环,每一行是一个for循环,两个循环嵌套在一起,第38行代码判断的是第第j行,第i个像素的像素值R分量是否为红色,其实判断的就是这个像素是否属于 第23行代码定义的文本“道”,如果判断属于,这时候就绘制一个圆,圆的直径占用的是6行6列方形像素区域,也就是第43行代码圆弧方法arc的第3个表示半径的参数d/2-0.8,为了使点阵中的点保持一定间隙,把直径6减小了0.8。 注意设置圆绘制的坐标,因为坐标系的原点位于画布中心,如果希望点阵位于画布中间,就把像素的纵横坐标分别减去400、300。

canvas点阵文字

canvas插入文字

  本节课的案例中涉及到了canvas插入文本的知识点,下面进行简单总结。

插入文本方法

  text表示要插入画布的字符串,x,y表示文字坐标。

方法 作用 效果 对应颜色属性
fillText(text,x,y) 内部填充文字,类比fill() fillStyle
strokeText(text,x,y) 外边线文字,类比stroke() strokeStyle

文字样式属性

  canvas的文字样式属性类似CSS文字样式属性,如果你已经掌握CSS的文字样式使用规律,对于canvas的字体样式不用专门学习,直接借助W3c查询即可。

canvas font 属性
CSS font 属性

canvas文本属性(文本定位)

  canvas的文本属性参照CSS文本属性制定,大致相同,个别书写方式和定位方式略有不同,比如canvas画布上对文本的定位要比普通HTML元素自由性更强,通过 fillText(text,x,y)或strokeText(text,x,y)方法的第2和3各参数直接写出文字位置。

  文本对齐方式:CSS文本对齐属性的写法是text-align,canvas文本对齐属性的写法是textAlign, CSS中定义文本居中对齐text-align: center参考基准是元素一行宽度的中间位置,canvas中定义文本居中对齐c.textAlign = "center"; 参考基准是fillText(text,x,y)或strokeText(text,x,y)方法定义的水平方向坐标值x。

  文本上下定位(垂直对齐方式):CSS使用vertical-align属性定义文本在一行中的显示位置,CSS以行的基线、中线为基准, canvas使用属性textBaseline定义,基准是 fillText(text,x,y)或strokeText(text,x,y)方法定义的高度方向坐标值y。比如c.textBaseline="middle"; 可以理解为文本中心线位置的纵坐标就是 fillText(text,x,y)或strokeText(text,x,y)方法定义的高度方向坐标值y。

canvas键盘控制运动

源码下载 体验测试

  利用canvas创建小游戏往往会通过键盘控制一个对象运动,下面代码作用就是通过键盘方向键控制一个静态大熊猫上下左右运动,比如按住向上方向键,大熊猫向上移动,松开方向键,大熊猫停止移动。

6    <body>
7    <div style="width: 800px;margin: auto">
8        <canvas id="canvas" width="800px" height="600px" style="background: #bbddff">
9        </canvas>
10   </div>
11   <script type="text/javascript">
12       let canvas=document.getElementById('canvas');
13       c=canvas.getContext('2d');
14       c.translate(400,300);
15       /*******************************
16        加载大熊猫图片
17        ********************************/
18       let img=new Image();
19       img.src = "gong.png";
20       let tr = false;//声明一个标志变量(标志:图片是完成加载)
21       img.onload = function () {
22           tr = true;//重置标志变量tr值
23       };
24       /*******************************
25        主循环函数:实时判断键盘标志变量key状态,更新熊猫位置,重绘新位置熊猫对象
26        ********************************/
27       let T0 = new Date();//上次调用时间
28       function main() {
29           let T1 = new Date();//本次调用时间
30           let t = T1 - T0;//函数调用时间差(单位:ms)
31           T0 = T1;
32           update(t / 1000);//调用大熊猫图片绘制函数(61行)
33           draw();//调用大熊猫图片绘制函数(41行)
34           requestAnimationFrame(main);//请求浏览器执行main函数
35       }
36   
37       /*******************************
38        大熊猫图片绘制函数
39        ********************************/
40       let x = 0, y = 0;//大熊猫初始位置坐标(x,y)
41       function draw() {
42           c.clearRect(-400,-300,800,600);//清除画布上一帧像素
43           if(tr){//判断图片是否加载完
44               c.drawImage(img,x,y,150,150);//绘制大熊猫图片
45           }
46       }
47       /*******************************
48       监听键盘状态,定义按键中断函数
49       ********************************/
50       let key = 0;//标志变量(键盘状态)
51       addEventListener("keydown",function (e) {
52           key = e.keyCode;//键盘被按下按键的返回值赋值给标志变量
53       });
54       addEventListener("keyup",function (e) {
55           key = 0;//按键松开,重置key为0
56       });
57       /*******************************
58        重置大熊猫位置坐标(x,y),位移 = 速度 X 时间
59        ********************************/
60       const speed = 300;//大熊猫平移速度(300px/s)
61       function update(time) {
62           if (key == 38) {//按键“↑”返回值keyCode=38
63               y -= speed * time;
64           }
65           if (key == 40) {
66               y += speed * time;//按键“↓”返回值keyCode=40
67           }
68           if (key == 37) {//按键“←”返回值keyCode=37
69               x -= speed * time;
70           }
71           if (key == 39) {//按键“→”返回值keyCode=39
72               x += speed * time;
73           }
74       }
75       //开启调用主循环函数
76       main();
77   </script>
78   </body>

代码解析

  要理解上面的代码,除了你要知道canvas、键盘事件等相关知识点以外,你要有一定的时间概念,不论基于裸奔的单片机,还是基于操作系统,还是基于浏览器开发程序,程序最简单的原则是自上而下执行,除此外 一般都会有一个主循环,主循环函数不停的循环执行,在主循环里面会根据需要调用相关功能函数,有些时候浏览器需要处理一些突发事件,比如键盘事件、鼠标事件,这时候就需要定义中断函数,实时监测键盘、鼠标等计算机外设的状态。 浏览器时间安排上,一般都是循环执行主函数,如果发生了外部键盘、鼠标等事件,主循环函数就会暂停执行,处理完中断中的程序,再回过头来执行主循环函数。(键盘)

  上面的代码初始化之后,浏览器在时间安排上就是不停地循环执行第28行定义的主循环函数main(),如果某个时刻你按下键盘,就会执行第52行通过addEventListener方法引入的中断代码,执行完里面的程序,又会回到 主循环函数,如果某个瞬间,你松开了按键,主循环函数的程序又暂停执行,跳转到第54行定义的按键松开中断函数,执行第55行代码,把全局变量k恢复原来的值0,执行完中断程序,再回到主循环函数程序。一句话概括就是 浏览器循环执行主程序的同时监控键盘状态。

  第18到第23行代码的作用是加载大熊猫的图片,前面的课程都是把第44行的绘制drawImage方法放在图片的onload加载完成函数中,为了在主函数第33行循环调用绘制函数执行drawImage方法,采用第 20行代码一个全局变量tr来表征图片是否加载完成,如果加载完成,就通过第22行代码更改全局变量的tr值,然后在第41行声明了一个绘制函数draw(),函数中在编写第44行代码的时候,首先使用一个if语句判断全局变量tr的值, 其实就是判断大熊猫的图片是否加载完成。

  第27到第36行代码的作用就是通过requestAnimationFrame方法循环调用main函数,第32行代码调用第61行定义的update()函数,通过执行该函数,判断键盘状态可以实时更新大熊猫的位置(x,y),大熊猫的位置更新后,第33行代码调用 绘制函数draw使用新的位置(x,y)参数重新渲染出熊猫图片。

  第40到第46行代码声明绘制函数draw供主循环函数循环调用,来实现动画效果,实际的动画,其实就是一帧一帧的图片叠加出的视觉效果。第42行代码的作用是清除画布上一帧的像素,如果不清除,两帧图片会叠加在一起,产生混乱的视觉效果。 在没有按键盘方向键的时候,执行drawImage方法,坐标是原点位置,x和y是变量,通过第61行声明的函数可以改变。

  第60到第74行代码功能是更新熊猫的位置坐标,在主函数中每调用一次,会通过update函数中的if语句判断键盘状态,计算坐标值x和y。键盘就相当于一个传感器,不同的按键被按下后会返回一个特定数字值, 在第51到第56行的中断函数外第50行声明了一个全局变量key,来记录键盘的状态值。比如你按下方向键“→”,第52行代码执行key=39,假设你持续按了1s,执行第72行代码,x的值增加300px,第32行代码函数参数是t/1000,就是把时间单位 从ms转化为s。

键盘方向键返回值

键盘方向键返回值

扩展(熊猫追赶游戏)

源码下载 体验测试

  在上面代码的基础上进行更改,再引入一张熊猫的图片,基本场景就是用键盘控制一个熊猫接近另外一个熊猫,当碰撞在的时候,被碰撞的熊猫自动重新随机更换位置,键盘控制的熊猫命名为gong,被撞的熊猫命名为shou。

  实现上面场景的核心就是判断两只熊猫的相撞条件,两只熊猫在画布上的位置是可以实时获取的,它们本身在画布上显示的尺寸是已知的,只要两只熊猫的坐标差值绝对值,小于等于它们的各自尺寸一半,就意味着两只熊猫相撞。 相撞的临界值不一定要设置为他们各自尺寸的一半之和,也可以比它小,你可以根据视觉效果自由设定。

  1.引入熊猫shou的图片,和第18到第23行代码除了变量名字全部相同

   /**熊猫shou**/
    let img2=new Image();
    img2.src = "shou.png";
    let tr2 = false;//声明一个标志变量(标志:图片是完成加载)
    img2.onload = function () {
        tr2 = true;//重置标志变量tr2值
    };

  2.第40到第46行代码定义的绘制函数draw()引入熊猫shou的绘制方法,这很好理解,两张熊猫的图片都要不停地绘制更新

   if(tr2){//判断图片shou是否加载完
        c.drawImage(img2,x2,y2,150,150);//绘制大熊猫shou图片
   }

  3.在主循环函数main()中添加下面的代码,判断两个熊猫是否相撞,如果相撞,就利用随机数方法random()重置熊猫shou的位置

   /**熊猫相撞判断,两张图片中心距150px,设置一个小于150的临界值,比如50**/
   if((x <= x2+50) && (x2 <= x+50)&& (y2 <= y+50)&& (y2 <= y+50)){
       x2 = 350*(Math.random()-0.5);//重置熊猫shou的横坐标
       y2 = 250*(Math.random()-0.5);//重置熊猫shou的总坐标
   }