HTML5简单入门系列(八)

时间:2022-03-04 17:02:22

前言

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

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

下边是公用的canvas标签:

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

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

 window.onload = function () {
shadow();
//transparent();
}

HTML5 APIs 续

透明

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

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

 function transparent() {
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.fillStyle = "red";
ctx.rect(, , , );
ctx.fill(); ctx.globalAlpha = 0.4;
ctx.beginPath();
ctx.fillStyle = "green";
ctx.rect(, , , );
ctx.fill(); ctx.globalAlpha = 0.2;
ctx.fillStyle = "blue";
ctx.fillText("Sample String", , );
}

效果如图:

HTML5简单入门系列(八)

阴影

要为图形(文本)添加阴影需要用到 shadowColor,shadowBlur,shadowOffsetX 和shadowOffsetY属性。

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

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

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

示例代码如下:

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

效果如下:

HTML5简单入门系列(八)

状态的保存和恢复

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

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

看下边的示例:

 var i, context, interval;
window.onload = function () {
//draw();//可在浏览器端 单步调试效果更佳。 i = ;
context = document.getElementById('myCanvas').getContext('2d');
interval = setInterval(dystate, );
}
function draw() {
var ctx = document.getElementById('myCanvas').getContext('2d'); ctx.fillRect(, , , ); // state 1
ctx.save(); ctx.fillStyle = '#09F'
ctx.fillRect(, , , ); // state 2
ctx.save(); ctx.fillStyle = '#FFF'
ctx.globalAlpha = 0.5;
ctx.fillRect(, , , ); //state 3 没有保存 ctx.restore(); //恢复到 state 2
ctx.fillRect(, , , ); ctx.restore(); // 恢复到state 1
ctx.fillRect(, , , );
} function dystate() {
var r1 = Math.floor((Math.random() * + ) * % );
var r2 = Math.floor((Math.random() * + ) * % );
var r3 = Math.floor((Math.random() * + ) * % );
context.fillStyle = "rgb(" + r1 + "," + r2 + "," + r3 + ")"; if (i <= ) {
context.fillRect(i * , i * , - i * , - i * );
context.save();
}
else {
context.restore();
context.fillRect(i * , i * , - i * , - i * );
}
i = i + ;
if (i > ) {
clearInterval(interval);
}
}

效果如下:

HTML5简单入门系列(八)

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

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

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

示例如下:

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

组合

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

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

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

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

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

摘自Mozilla开发社区

source-over (default)

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

HTML5简单入门系列(八)

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

HTML5简单入门系列(八)

source-in

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

HTML5简单入门系列(八)

destination-in

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

HTML5简单入门系列(八)

source-out

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

HTML5简单入门系列(八)

destination-out

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

HTML5简单入门系列(八)

source-atop

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

HTML5简单入门系列(八)

destination-atop

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

HTML5简单入门系列(八)

lighter

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

HTML5简单入门系列(八)

darker

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

HTML5简单入门系列(八)

xor

重叠的部分会变成透明。

HTML5简单入门系列(八)

copy

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

HTML5简单入门系列(八)

示例代码:

这里需要重新定义两个canvas

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

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

 window.onload = function () {
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
// 注意这里创建了一个临时canvas,可以理解为内存中绘图所用,用于在正式将图形画到页面之前先把完整的图形在这个临时canvas中画完,然后再一下子拷贝到真正用于显示的 myCanvas上,而在页面中的这个临时canvas是不可见的
var tempCanvas = document.getElementById("tempCanvas");
var tempContext = tempCanvas.getContext("2d"); var squareWidth = ;
var circleRadius = ;
var startX = ;
var startY = ;
var rectCircleDistX = ;
var rectCircleDistY = ;
var exampleDistX = ;
var exampleDistY = ;
var arr = new Array();
arr.push("source-atop");
arr.push("source-in");
arr.push("source-out");
arr.push("source-over");
arr.push("destination-atop");
arr.push("destination-in");
arr.push("destination-out");
arr.push("destination-over");
arr.push("lighter");
arr.push("darker");
arr.push("xor");
arr.push("copy");
// 画出十二种操作模式
for (var n = ; n < arr.length; n++) {
var thisX; var thisY;
var thisOperation = arr[n];
// 第一行
if (n < ) {
thisX = startX + (n * exampleDistX);//横坐标移到下个位置
thisY = startY;
}
// 第二行
else if (n < ) {
thisX = startX + ((n - ) * exampleDistX);//横坐标回到第二行起始位置,并后移
thisY = startY + exampleDistY;//纵坐标移到第二行
}
// 第三行
else {
thisX = startX + ((n - ) * exampleDistX);
thisY = startY + (exampleDistY * );
} tempContext.clearRect(, , canvas.width, canvas.height);//整个canvas被清空
// 画矩形
tempContext.beginPath();
tempContext.rect(thisX, thisY, squareWidth, squareWidth);
tempContext.fillStyle = "blue";
tempContext.fill(); // 设置全局组合模式
tempContext.globalCompositeOperation = thisOperation;
// 画圆
tempContext.beginPath();
tempContext.arc(thisX + rectCircleDistX, thisY + rectCircleDistY, circleRadius, , * Math.PI, false);
tempContext.fillStyle = "red";
tempContext.fill();
// 恢复成默认状态
tempContext.globalCompositeOperation = "source-over";
tempContext.font = "10pt Verdana";
tempContext.fillStyle = "black";
tempContext.fillText(thisOperation, thisX, thisY + squareWidth + );
// 将图像从 tempCanvas 拷贝到 myCanvas
context.drawImage(tempCanvas, , );
}
}

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

效果图如下:

HTML5简单入门系列(八)

裁切路径

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

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

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

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

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

 <!DOCTYPE html>
<html>
<body>
<p>不使用 clip():</p>
<canvas id="myCanvas" width="" height="" style="border: 1px solid red;">Your browser does not support the HTML5 canvas tag.
</canvas> <script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d"); ctx.rect(, , , );
ctx.strokeStyle = "blue";
ctx.stroke(); ctx.fillStyle = "green";
ctx.fillRect(, , , );
</script> <br /> <p>使用 clip():</p>
<canvas id="myCanvas2" width="" height="" style="border: 1px solid red;">Your browser does not support the HTML5 canvas tag.
</canvas> <script>
var c = document.getElementById("myCanvas2");
var ctx = c.getContext("2d");
// Clip a rectangular area
ctx.rect(, , , );
ctx.strokeStyle = "blue";
ctx.stroke();
ctx.clip(); ctx.fillStyle = "green";
ctx.fillRect(, , , );
</script>
</body>
</html>

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

HTML5简单入门系列(八)

移动

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

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

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

看下边的示例:

 function randcircle() {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
for (var x = ; x < ; x++) {
for (var y = ; y < ; y++) {
ctx.save();
//circle(ctx, x * 20, y * 20, 10);
ctx.translate(x * , y * );
circle(ctx, , , );
ctx.restore();
}
}
}
function circle(ctx, x, y, R) {
var r1 = Math.floor((Math.random() * + ) * % );
var r2 = Math.floor((Math.random() * + ) * % );
var r3 = Math.floor((Math.random() * + ) * % );
ctx.beginPath();
ctx.fillStyle = "rgb(" + r1 + "," + r2 + "," + r3 + ")"; //var s = Math.floor((Math.random() * 10 + 5) * 10 % 10);
//ctx.scale(s, s); ctx.arc(x, y, R, , * Math.PI, false);
ctx.fill();
}

看代码7-9行

 //circle(ctx, x * 20, y * 20, 10);
ctx.translate(x * , y * );
circle(ctx, , , );

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

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

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

HTML5简单入门系列(八)

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

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

HTML5简单入门系列(八)

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

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

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

HTML5简单入门系列(八)

缩放

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

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

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

 function scale() {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.strokeRect(, , , );
ctx.scale(, );
ctx.strokeRect(, , , );
ctx.scale(, );
ctx.strokeRect(, , , );
ctx.scale(, );
ctx.strokeRect(, , , );
}

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

HTML5简单入门系列(八)

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

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

旋转

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

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

 function rotate() {
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
context.translate(, );
context.arc(, , , , Math.PI * , false);//中心一个圆
context.fill();
for (var i = ; i < ; i++) {//层数
var x = i * ;//x坐标
var n = i * ;//该层圆圈数量
var s = * Math.PI * x;//当前x坐标圆周长,用于计算小圆的半径
var r = Math.floor(s / n / );//半径
var angle = * Math.PI / n;//旋转角度
for (var j = ; j < n; j++) {//每层是6的倍数递增
context.save();
context.beginPath();
context.rotate(angle * j);
context.arc(x, , r, , * Math.PI, false);
context.fillStyle = 'rgb(' + ( * i) + ',' + ( - * i) + ',255)';
context.fill();
context.restore();
}
}
}

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

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

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

效果如下图:

HTML5简单入门系列(八)

变换矩阵

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

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

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

HTML5简单入门系列(八)

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

参数说明如下:

参数

描述

a

水平缩放绘图

b

水平倾斜绘图

c

垂直倾斜绘图

d

垂直缩放绘图

e

水平移动绘图

f

垂直移动绘图

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

 function transform() {
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d"); var sin = Math.sin(Math.PI / );
var cos = Math.cos(Math.PI / );
ctx.translate(, );
var c = ;
for (var i = ; i <= ; i++) {
c = Math.floor( / * i);
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
ctx.fillRect(, , , );
ctx.transform(cos, sin, -sin, cos, , );
} ctx.setTransform(-, , , , , );
ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
ctx.fillRect(, , , );
}

先上效果图再解释:

HTML5简单入门系列(八)

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

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

所以,我添加如下代码

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

现在的效果图如下:

HTML5简单入门系列(八)

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

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

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

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

ctx.fillRect(, , , );

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

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

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

ctx.setTransform(-, 0.4, , , , );

效果如下:

HTML5简单入门系列(八)

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

HTML5简单入门系列(八)

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

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

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

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

小结

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

参考资料

w3school

Mozilla开发者社区

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

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

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


写博小感

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

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

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

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

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