HTML5简单入门系列(八)

前言

本篇介绍HTML5中相对复杂的一些APIs,其中的数学知识比较多。虽然如此,但是其实API使用起来还是比较方便的。

这里说明一下,只写出API相关的JS代码,因为他们都是基于一个canvas标签进行的操作。有特殊情况,我会单独列出。

下边是公用的canvas标签:

<canvas height="600" width="800" id="myCanvas"></canvas>

调用方式,也基本一致,如下:

1 window.onload = function () {
2             shadow();
3             //transparent();
4         }

HTML5 APIs 续

透明

设置图形的透明度要用到 globalAlpha 属性。globalAlpha 属性的值是一个介于之间的浮点数。0表示完全透明,而1表示完全不透明。

示例代码如下(注意画图时重新调用beginPath,否则后边的fill方法会连同之前的图形区域一起重新填充颜色): 

 1 function transparent() {
 2             var canvas = document.getElementById("myCanvas");
 3             var ctx = canvas.getContext("2d");
 4             ctx.globalAlpha = 0.7;
 5             ctx.beginPath();
 6             ctx.fillStyle = "red";
 7             ctx.rect(0, 0, 100, 100);
 8             ctx.fill();
 9 
10             ctx.globalAlpha = 0.4;
11             ctx.beginPath();
12             ctx.fillStyle = "green";
13             ctx.rect(50, 50, 150, 150);
14             ctx.fill();
15 
16             ctx.globalAlpha = 0.2;
17             ctx.fillStyle = "blue";
18             ctx.fillText("Sample String", 40, 60);
19         }

 效果如图:

 阴影

要为图形(文本)添加阴影需要用到 shadowColorshadowBlurshadowOffsetX shadowOffsetY属性。

shadowOffsetX 和 shadowOffsetY 用来设定阴影在轴的延伸距离,负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,默认都是0(像素)

shadowBlur 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵(后边会说到,变换矩阵会影响缩放、旋转和移动)影响,默认为0(w3school定义是阴影的模糊级数,默认值#000000)

shadowColor 用于设定阴影效果的延伸,值可以是标准的CSS 颜色值,默认是全透明的黑色。

示例代码如下: 

 1 function shadow() {
 2             var canvas = document.getElementById("myCanvas");
 3             var ctx = canvas.getContext("2d");
 4 
 5             ctx.shadowOffsetX = 5;//指示阴影位于形状(文本) left 位置右侧的 5 像素处
 6             ctx.shadowOffsetY = 5;
 7             ctx.shadowBlur = 2;
 8             ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
 9 
10             ctx.font = "40px Times New Roman";
11             ctx.fillStyle = "Black";
12             ctx.fillText("Sample String", 5, 60);
13         }

 效果如下:

状态的保存和恢复

save restore 方法是用来保存和恢复canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。

Canvas 状态是以栈(stack)的方式保存的,每一次调用save 方法,当前的状态就会被放入栈中保存起来。你可以调用任意多次save 方法。每一次调用restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。

看下边的示例: 

 1 var i, context, interval;
 2         window.onload = function () {
 3             //draw();//可在浏览器端 单步调试效果更佳。
 4 
 5             i = 1;
 6             context = document.getElementById('myCanvas').getContext('2d');
 7             interval = setInterval(dystate, 500);
 8         }
 9         function draw() {
10             var ctx = document.getElementById('myCanvas').getContext('2d');
11 
12             ctx.fillRect(0, 0, 150, 150);   // state 1
13             ctx.save();
14 
15             ctx.fillStyle = '#09F'
16             ctx.fillRect(15, 15, 120, 120);  // state 2
17             ctx.save();
18 
19             ctx.fillStyle = '#FFF'
20             ctx.globalAlpha = 0.5;
21             ctx.fillRect(30, 30, 90, 90);   //state 3 没有保存
22 
23             ctx.restore();               //恢复到 state 2
24             ctx.fillRect(45, 45, 60, 60);
25 
26             ctx.restore();               // 恢复到state 1
27             ctx.fillRect(60, 60, 30, 30);
28         }
29 
30         function dystate() {
31             var r1 = Math.floor((Math.random() * 2 + 1) * 255 % 255);
32             var r2 = Math.floor((Math.random() * 3 + 1) * 255 % 255);
33             var r3 = Math.floor((Math.random() * 7 + 1) * 255 % 255);
34             context.fillStyle = "rgb(" + r1 + "," + r2 + "," + r3 + ")";
35 
36             if (i <= 5) {
37                 context.fillRect(i * 10, i * 10, 150 - i * 15, 150 - i * 15);
38                 context.save();
39             }
40             else {
41                 context.restore();
42                 context.fillRect(i * 10, i * 10, 150 - i * 15, 150 - i * 15);
43             }
44             i = i + 1;
45             if (i > 10) {
46                 clearInterval(interval);
47             }
48         }

 效果如下:

该示例中,LZ使用了两个示例,第一个示例单步调试效果和第二个示例效果基本一样。

第二个示例是随机颜色值,每次执行结果不同,请注意!

代码中标出的红色部分rgb方法就是获取颜色值,rgba() 方法与 rgb() 方法类似,就多了一个用于设置色彩透明度的参数。它的有效范围是从 0.0(完全透明)到 1.0(完全不透明)。

示例如下:

1 // 这些 fillStyle 的值均为 '橙色'
2 ctx.fillStyle = "orange";
3 ctx.fillStyle = "#FFA500";
4 ctx.fillStyle = "rgb(255,165,0)";
5 ctx.fillStyle = "rgba(255,165,0,1)";

组合

之前的例子里面,我们总是将一个图形画在另一个之上,大多数情况下,这样是不够的。比如说,它这样受制于图形的绘制顺序(透明度那个示例可说明,后绘画的会覆盖之前的绘画)。不过,我们可以利用 globalCompositeOperation 属性来改变这些做法。 

globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有)的图像上。

源图像 = 您打算放置到画布上的绘图。 

目标图像 = 您已经放置在画布上的绘图。 

默认值: source-over
JavaScript 语法: context.globalCompositeOperation="source-in";

 摘自Mozilla开发社区

source-over (default)

这是默认设置,新图形会覆盖在原有内容之上。

destination-over
会在原有内容之下绘制新图形。

source-in

新图形会仅仅出现与原有内容重叠的部分。其它区域都变成透明的。

destination-in

原有内容中与新图形重叠的部分会被保留,其它区域都变成透明的。

source-out

结果是只有新图形中与原有内容不重叠的部分会被绘制出来。

destination-out

原有内容中与新图形不重叠的部分会被保留。

source-atop

新图形中与原有内容重叠的部分会被绘制,并覆盖于原有内容之上。

destination-atop

原有内容中与新内容重叠的部分会被保留,并会在原有内容之下绘制新图形

lighter

两图形中重叠部分作加色处理。

darker

两图形中重叠的部分作减色处理。

xor

重叠的部分会变成透明。

copy

只有新图形会被保留,其它都被清除掉。

示例代码:

这里需要重新定义两个canvas

1 <canvas id="myCanvas" width="578" height="430"></canvas>
2     <!-- 下面这个canvas就是用作内存中绘图的,样式被设为不可见 -->
3     <canvas id="tempCanvas" width="578" height="430" style="display: none;"></canvas>

下边的代码将会绘制如上表中的图形原型(代码来源

 1 window.onload = function () {
 2             var canvas = document.getElementById("myCanvas");
 3             var context = canvas.getContext("2d");
 4             // 注意这里创建了一个临时canvas,可以理解为内存中绘图所用,用于在正式将图形画到页面之前先把完整的图形在这个临时canvas中画完,然后再一下子拷贝到真正用于显示的 myCanvas上,而在页面中的这个临时canvas是不可见的
 5             var tempCanvas = document.getElementById("tempCanvas");
 6             var tempContext = tempCanvas.getContext("2d");
 7 
 8             var squareWidth = 55;
 9             var circleRadius = 35;
10             var startX = 10;
11             var startY = 30;
12             var rectCircleDistX = 50;
13             var rectCircleDistY = 50;
14             var exampleDistX = 150;
15             var exampleDistY = 140;
16             var arr = new Array();
17             arr.push("source-atop");
18             arr.push("source-in");
19             arr.push("source-out");
20             arr.push("source-over");
21             arr.push("destination-atop");
22             arr.push("destination-in");
23             arr.push("destination-out");
24             arr.push("destination-over");
25             arr.push("lighter");
26             arr.push("darker");
27             arr.push("xor");
28             arr.push("copy");
29             // 画出十二种操作模式
30             for (var n = 0; n < arr.length; n++) {
31                 var thisX; var thisY;
32                 var thisOperation = arr[n];
33                 // 第一行     
34                 if (n < 4) {
35                     thisX = startX + (n * exampleDistX);//横坐标移到下个位置
36                     thisY = startY;
37                 }
38                     // 第二行          
39                 else if (n < 8) {
40                     thisX = startX + ((n - 4) * exampleDistX);//横坐标回到第二行起始位置,并后移
41                     thisY = startY + exampleDistY;//纵坐标移到第二行
42                 }
43                     // 第三行        
44                 else {
45                     thisX = startX + ((n - 8) * exampleDistX);
46                     thisY = startY + (exampleDistY * 2);
47                 }
48 
49                 tempContext.clearRect(0, 0, canvas.width, canvas.height);//整个canvas被清空
50                 // 画矩形
51                 tempContext.beginPath();
52                 tempContext.rect(thisX, thisY, squareWidth, squareWidth);
53                 tempContext.fillStyle = "blue";
54                 tempContext.fill();
55 
56                 // 设置全局组合模式
57                 tempContext.globalCompositeOperation = thisOperation;
58                 // 画圆
59                 tempContext.beginPath();
60                 tempContext.arc(thisX + rectCircleDistX, thisY + rectCircleDistY, circleRadius, 0, 2 * Math.PI, false);
61                 tempContext.fillStyle = "red";
62                 tempContext.fill();
63                 // 恢复成默认状态
64                 tempContext.globalCompositeOperation = "source-over";
65                 tempContext.font = "10pt Verdana";
66                 tempContext.fillStyle = "black";
67                 tempContext.fillText(thisOperation, thisX, thisY + squareWidth + 45);
68                 // 将图像从 tempCanvas 拷贝到 myCanvas 
69                 context.drawImage(tempCanvas, 0, 0);
70             }
71         }

关于为什么需要一个临时的canvas,这就和globalCompositeOperation 属性相关了,LZ单步调试了,后边的绘图(部分)会将之前的内容清空,比如最后一个copy值,会清空已有的所有图形而单独显示它自己,这是copy的本质,其他几个属性值,也有类似行为,因此需要一个临时canvas,一个一个拷贝到展示的canvas上。

效果图如下:

裁切路径

clip() 方法从原始画布中剪切任意形状和尺寸。

一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。

如果和上面介绍的 globalCompositeOperation 属性作一比较,它可以实现与source-in source-atop 差不多的效果。最重要的区别是裁切路径不会在canvas 上绘制东西,而且它不受新图形的影响。

默认情况下,canvas 有一个与它自身一样大的裁切路径(也就是绘图只能在canvas范围内)。

我们使用两个canvas对比使用clip和不使用clip时,对绘图效果的影响。

 1 <!DOCTYPE html>
 2 <html>
 3 <body>
 4     <p>不使用 clip():</p>
 5     <canvas id="myCanvas" width="300" height="150" style="border: 1px solid red;">Your browser does not support the HTML5 canvas tag.
 6     </canvas>
 7 
 8     <script>
 9         var c = document.getElementById("myCanvas");
10         var ctx = c.getContext("2d");
11         
12         ctx.rect(50, 20, 200, 120);
13         ctx.strokeStyle = "blue";
14         ctx.stroke();
15         
16         ctx.fillStyle = "green";
17         ctx.fillRect(0, 0, 150, 100);
18     </script>
19 
20     <br />
21 
22     <p>使用 clip():</p>
23     <canvas id="myCanvas2" width="300" height="150" style="border: 1px solid red;">Your browser does not support the HTML5 canvas tag.
24     </canvas>
25 
26     <script>
27         var c = document.getElementById("myCanvas2");
28         var ctx = c.getContext("2d");
29         // Clip a rectangular area
30         ctx.rect(50, 20, 200, 120);
31         ctx.strokeStyle = "blue";
32         ctx.stroke();
33         ctx.clip();
34         
35         ctx.fillStyle = "green";
36         ctx.fillRect(0, 0, 150, 100);
37     </script>
38 </body>
39 </html>

效果如下(红色是canvas整个区域,蓝色是裁剪区,绿色是填充效果):

移动

translate(x, y) 它用来移动 canvas 和它的原点到一个不同的位置。

默认初始时,canvas的原点坐标是(0,0),我们可以把canvas看做一个原点坐标是(0,0)的坐标系。使用该方法后,整个canvas位移到(x,y)为原点的坐标系。之后的画图操作数值都是相对于移动之后的坐标系的,如:

位移之后,某坐标位置是(100,100),则相对于移动之前的坐标系,它是(100+x,100+y),原坐标系的(x,y)是当前坐标系的(0,0)原点。

看下边的示例: 

 1 function randcircle() {
 2             var c = document.getElementById("myCanvas");
 3             var ctx = c.getContext("2d");
 4             for (var x = 5; x < 10; x++) {
 5                 for (var y = 5; y < 10; y++) {
 6                     ctx.save();
 7                     //circle(ctx, x * 20, y * 20, 10);
 8                     ctx.translate(x * 20, y * 20);
 9                     circle(ctx, 0, 0, 10);
10                     ctx.restore();
11                 }
12             }
13         }
14         function circle(ctx, x, y, R) {
15             var r1 = Math.floor((Math.random() * 2 + 1) * 255 % 255);
16             var r2 = Math.floor((Math.random() * 3 + 1) * 255 % 255);
17             var r3 = Math.floor((Math.random() * 7 + 1) * 255 % 255);
18             ctx.beginPath();
19             ctx.fillStyle = "rgb(" + r1 + "," + r2 + "," + r3 + ")";
20 
21             //var s = Math.floor((Math.random() * 10 + 5) * 10 % 10);
22 //ctx.scale(s, s); 23 24 ctx.arc(x, y, R, 0, 2 * Math.PI, false); 25 ctx.fill(); 26 }

 看代码7-9行

7 //circle(ctx, x * 20, y * 20, 10);
8 ctx.translate(x * 20, y * 20);
9 circle(ctx, 0, 0, 10);

注释的这一句可以当做下边两句来使用,但这并没能将translate的优势展示出来。

我们可以想象,circle方法内如果是定点画圆呢?也就是说circle如果不接收位置参数,那么先移动坐标原点再调用方法则可以在随意位置画圆了。

 效果图如下(颜色是随机的):

值得注意的是,代码中21-22行注释掉的缩放代码,对圆心的定位影响很大,LZ还需要再研究一下。先看一下效果图吧。

1、将21-22代码取消注释,其他不变,效果如下(效果同LZ所想):

2、将21-22代码取消注释,修改代码如下

7 circle(ctx, x * 20, y * 20, 10);
8 //ctx.translate(x * 20, y * 20);
9 //circle(ctx, 0, 0, 10);

效果如图(LZ略晕,因为圆心位置变了):

缩放

scale 方法接受两个参数。x,y 分别是横轴和纵轴的缩放因子,它们都必须是正值。值比1.0 小表示缩小,比1.0 大则表示放大,值为1.0 时什么效果都没有。

如果您对绘图进行缩放,所有之后的绘图也会被缩放。定位也会被缩放,如果您 scale(2,2),那么绘图将定位于距离画布左上角两倍远的位置。

看到标红的文字,LZ释然了,上边的例子中的疑惑也就澄清了。让我们来看个示例再对比一下效果吧。 

 1 function scale() {
 2             var c = document.getElementById("myCanvas");
 3             var ctx = c.getContext("2d");
 4             ctx.strokeRect(5, 5, 25, 15);
 5             ctx.scale(2, 2);
 6             ctx.strokeRect(5, 5, 25, 15);
 7             ctx.scale(2, 2);
 8             ctx.strokeRect(5, 5, 25, 15);
 9             ctx.scale(2, 2);
10             ctx.strokeRect(5, 5, 25, 15);
11         }

 我们画了是个矩形,每次横向纵向放大2倍,效果如下:

 

虽然每次都是从(5,5)到(25,15)的矩形,但是缩放之后,不止长宽变了,起点坐标也跟着一起变化了

而上边translate例子中,移动原点位置之后再缩放,则只影响长度不影响定位

旋转

rotate(angle)这个方法只接受一个参数:旋转的弧度(angle),它是顺时针方向的,以弧度为单位的值。如果是角度,则可以使用 degrees*Math.PI/180转换成弧度。

旋转的中心点始终是 canvas 的原点,如果要改变它,需要用到 translate 方法。该方法比较简单,看下边的示例:

 1 function rotate() {
 2             var canvas = document.getElementById("myCanvas");
 3             var context = canvas.getContext("2d");
 4             context.translate(400, 300);
 5             context.arc(0, 0, 10, 0, Math.PI * 2, false);//中心一个圆
 6             context.fill();
 7             for (var i = 1; i < 8; i++) {//层数
 8                 var x = i * 20;//x坐标
 9                 var n = i * 6;//该层圆圈数量
10                 var s = 2 * Math.PI * x;//当前x坐标圆周长,用于计算小圆的半径
11                 var r = Math.floor(s / n / 2);//半径
12                 var angle = 2 * Math.PI / n;//旋转角度
13                 for (var j = 0; j < n; j++) {//每层是6的倍数递增
14                     context.save();
15                     context.beginPath();
16                     context.rotate(angle * j);
17                     context.arc(x, 0, r, 0, 2 * Math.PI, false);
18                     context.fillStyle = 'rgb(' + (30 * i) + ',' + (255 - 30 * i) + ',255)';
19                     context.fill();
20                     context.restore();
21                 }
22             }
23         }

首先我们先移动原点到(400,300),然后画了一个小圆。

我们设定圆圈展示层数、及每层数量,然后根据当前层所在坐标及每层数量(6倍递增)计算当前层小圆的半径和旋转角度(弧度)

最后在同一个位置画圆,再将小圆旋转到正确位置(记得每次save和restore,否则旋转角度不必angle * j,而是继承上次旋转角度;还有每次绘图之前要用beginPath哦!)

效果如下图:

变换矩阵

transform(a,b,c,d,e,f) 方法以用户自定义的变换矩阵对图像坐标进行变换操作

setTransform(a,b,c,d,e,f) 方法重置当前的变形矩阵为单位矩阵,然后以相同的参数调用 transform 方法。(反正LZ是有点晕~~~)

这个方法需要6个参数组成一个 3 x 3 的转换矩阵,坐标由 (x, y) 到 (x', y') 的转换公式如下所示:

 这个。。计算。。呵呵。。记住下边的参数说明就行了。。 

参数说明如下:

参数

描述

a

水平缩放绘图

b

水平倾斜绘图

c

垂直倾斜绘图

d

垂直缩放绘图

e

水平移动绘图

f

垂直移动绘图

我们还是看下边的例子吧。

 1 function transform() {
 2             var canvas = document.getElementById("myCanvas");
 3             var ctx = canvas.getContext("2d");
 4 
 5             var sin = Math.sin(Math.PI / 6);
 6             var cos = Math.cos(Math.PI / 6);
 7             ctx.translate(200, 200);
 8             var c = 0;
 9             for (var i = 0; i <= 12; i++) {
10                 c = Math.floor(255 / 12 * i);
11                 ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
12                 ctx.fillRect(0, 0, 100, 10);
13                 ctx.transform(cos, sin, -sin, cos, 0, 0);
14             }
15 
16             ctx.setTransform(-1, 0, 0, 1, 200, 200);
17             ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
18             ctx.fillRect(0, 50, 100, 100);
19         }

先上效果图再解释:

看到效果,各位园友觉得这个矩形是怎么画出来的呢?此时的坐标原点在哪里呢?

好吧,LZ得承认最开始这个旋转的横条效果,也没有搞的很明白,虽然单步调试看到了效果。。。对于这个矩形呢,LZ也没有找到正确的坐标原点。

所以,我添加如下代码

ctx.strokeStyle = "green";
ctx.arc(0, 0, 50, 0, Math.PI, false);
ctx.stroke();

现在的效果图如下:

现在找到了坐标原点了吧,对,它就在绿色弧线中心。

这里LZ要强调一下,setTransform方法会重置当前的变形矩阵为单位矩阵,即原始没有旋转、没有缩放、没有移动的原始坐标系。

所以setTransform第5/6两个参数将作为移动(translate)参数进行原点移动,就到了上图中绿色弧线的中心了。

那么接下来,根据绘制矩形的坐标设置,我们想想它是怎样一个效果。

ctx.fillRect(0, 50, 100, 100);

起点(0,50)没问题,那么左下角(100,100)的横坐标怎么偏向左边去了呢?

这就是我们在调用setTransform是第一个参数的效果,水平缩放-1,即长度不变,方向相反。第四个参数垂直缩放1,保持原来的效果。

我们再修改代码,看看第二个、第三个参数的效果。修改如下:

ctx.setTransform(-1, 0.4, 0, 1, 200, 200);

效果如下:

我们修改了第二参数,水平倾斜0.4(倾斜和旋转都是顺时针的),但是效果图是逆时针倾斜的,这和第一个参数-1有关,我们将第一个参数改成1,效果如下,符合我们的预期:

那么到这里,应该这几个参数都弄明白了吧。

我们介绍的只是setTransform方法,它和transform的使用是一样的。

区别在于,setTransform会先将坐标系重置即所谓的单位矩阵,然后在参数基础上执行transform方法。

关于那个'米'字运行效果,就不再做解释了。

小结

本篇介绍了透明、阴影、状态的保存和恢复、组合、裁剪路径、移动、缩放、旋转及变换矩阵,东西略多,楼主也是搜罗了好多地方才整理好。

参考资料

w3school

Mozilla开发者社区

canvas教程 (页面左下角的文章列表)

如果有些说明错误的地方,请园友大牛指正。

后边还有一些关于动画的东西,但是和HTML5 APIs 没什么直接关系,都是使用现有js技术结合HTML5 APIs来实现的动态效果,有兴趣的可以自己搜索一下。


 写博小感

通过写该系列博客,楼主真心感觉到写博的不容易,单是本篇,楼主用了整整一天时间来完成。

虽然本系列只是楼主的学习记录,但是写出来的时候,心里总是胆战心惊,这里没弄明白写错了怎么办?让园友大牛笑话怎么办?误导其他人怎么办?

尤其是最近两篇APIs的介绍,每个参数是做什么的、最后有什么效果,楼主都不敢妄想,从各处搜索资料,一一实验(虽然有些代码并非原创),然后将自己的想法描述出来。

尽管耗时耗力,但是终归算是有了一个结束,楼主本人也颇有收获,也终于明白为什么前人强调要写博客的重要性,就算是很细小很简单的知识点,说不定就能解决别人在这个节骨眼上碰到麻烦。

好了,就说这些吧,欢迎拍砖(凡是打不死我的,终将让我变得更加坚强,O(∩_∩)O哈哈~)

原文地址:https://www.cnblogs.com/cotton/p/4409715.html