前言
本篇介绍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 之间的浮点数。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 }
效果如图:
阴影
要为图形(文本)添加阴影需要用到 shadowColor,shadowBlur,shadowOffsetX 和shadowOffsetY属性。
shadowOffsetX 和 shadowOffsetY 用来设定阴影在X 和Y 轴的延伸距离,负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,默认都是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"; |
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方法。
关于那个'米'字运行效果,就不再做解释了。
小结
本篇介绍了透明、阴影、状态的保存和恢复、组合、裁剪路径、移动、缩放、旋转及变换矩阵,东西略多,楼主也是搜罗了好多地方才整理好。
参考资料
canvas教程 (页面左下角的文章列表)
如果有些说明错误的地方,请园友大牛指正。
后边还有一些关于动画的东西,但是和HTML5 APIs 没什么直接关系,都是使用现有js技术结合HTML5 APIs来实现的动态效果,有兴趣的可以自己搜索一下。
写博小感
通过写该系列博客,楼主真心感觉到写博的不容易,单是本篇,楼主用了整整一天时间来完成。
虽然本系列只是楼主的学习记录,但是写出来的时候,心里总是胆战心惊,这里没弄明白写错了怎么办?让园友大牛笑话怎么办?误导其他人怎么办?
尤其是最近两篇APIs的介绍,每个参数是做什么的、最后有什么效果,楼主都不敢妄想,从各处搜索资料,一一实验(虽然有些代码并非原创),然后将自己的想法描述出来。
尽管耗时耗力,但是终归算是有了一个结束,楼主本人也颇有收获,也终于明白为什么前人强调要写博客的重要性,就算是很细小很简单的知识点,说不定就能解决别人在这个节骨眼上碰到麻烦。
好了,就说这些吧,欢迎拍砖(凡是打不死我的,终将让我变得更加坚强,O(∩_∩)O哈哈~)