JavaScript权威指南 第15章 网络编程 第三部分
可伸缩矢量图形
SVG(Scalable Vector Graphics,可伸缩矢量图形)是一种图片格式。名字中的“矢量”代表着它与GIF、JPEG、PNG等指定像素值矩阵的光栅图片格式有着根本的不同。SVG“图片”有一种对绘制期望图形的精确的、分辨率无关(因而“可伸缩”)的描述。SVG图片是在文本文件中通过(与HTML类似的)XML标记语言描述的。
在浏览器中有几种的方式使用SVG:
- 可以在常规的HTML< img >标签中使用.svg图片文件,就像使用.png或.jpeg图片一样。
- 因为基于XML的SVG格式与HTML很类似,所以可以直接把SVG标签嵌入在HTML文档中。此时,浏览器的HTML解析器允许省略XML命名空间,并将SVG标签当成HTML标签一样处理。
- 可以使用DOM API动态创建SVG元素,按需生成图片
接下来几小节将演示SVG的第二种和第三种用法。不过,要注意SVG本身的语法规则很多,还是比较复杂的。除了简单的图形绘制语法,SVG还支持任意曲线、文本和动画。SVG图形甚至可以与JavaScript脚本和CSS样式表组合,与添加行为和表现信息。完整介绍SVG确实超出了本书范围。本节的目标仅限于展示如何在HTML文档中使用SVG,以及通过JavaScript来操控它。
15.7.1 在HTML中使用SVG
SVG图片当然可以使用HTML的< img>标签来显示,但也可以直接在HTML嵌入SVG。而且在嵌入SVG中,甚至可以使用CSS样式来指定字体、颜色和线宽。
< svg>标签的后代并非标准的HTML标签。还要注意,CSS简写的font属性对SVG标签不起作用,因此必须要分别设置font-family、font-size和font-weight属性。
15.7.2 编程操作SVG
直接在HTML文件中嵌入SVG(而不是使用静态< img>标签)的一个原因,就是这样可以使用DOM API操作SVG图片。假设你想使用SVG在网页中显示一个图标。可以把SVG嵌入一个< template>标签中,然后在需要向UI中传入图标副本时就克隆这个模板的内容。如果想让图标响应用户活动(比如在鼠标指针悬停在图标上时改变颜色),那通常可以使用CSS来实现。
操作直接嵌入在HTML的SVG图形也是可能的。上一节中的那个表盘的示例显示的是一个静态时钟,时钟和分针都指向正上方,表明时间为中午或半夜。不过有读者可能也注意到了,这个示例的HTML文件中包含一个< script>标签。这个表情引入的脚本会周期性地运行一个函数,该函数会检查并旋转时钟和分钟对应相应地度数,从而让时钟真正反映当前时间。
操作设置地代码很好理解。它会根据当前时间来确定时针和分针地适当角度,然后使用querySelector()找到显示这两个表针的SVG元素,设置它们的transform属性,围绕表盘的中心旋转相应的角度。
(function updateClock(){ //更新SVG时钟,显示当前时间
let now=new Date(); //当前时间
let sec=now.getSeconds(); //秒
let min=now.getMinutes()+sec/60; //分数形式的分钟
let hour=now.getHours()+min/60; //分数形式的小时
let minangle=min*6; //每分钟6度
let hourangle=hour*30; //每小时30度
//取得显示表针的SVG元素
let minhand=document.querySelector("#clock .minutehand");
let hourhand=document.querySelector("#clock .hourhand");
//设置SVG属性,围绕表盘移动指针
minhand.setAttribute("transform",`rotate(${minangle},50,50)`);
hourhand.setAttribute("transform",`rotate(${hourangle},50,50)`);
//10秒之后再次运行这个函数
setTimeout(updateClock,10000);
}()); //注意这里立即调用函数
15.7.3 通过JavaScript创建SVG图片
尽管可以把SVG标签包含在HTML文档中,严格来讲它们仍然是XML标签,不是HTML标签。如果想通过JavaScript DOM API创建SVG元素,那就不能使用15.3.5介绍的createElement()函数,而必须使用cleateElementNS(),这个函数的第一个参数是XML命名空间字符串。对SVG而言,命名空间是字符串“http://www.w3.org/2000/svg”。
除了使用createElementNS(),示例15-4中绘制饼图的代码都比较任意理解。只有把要绘制的数据转换为扇形的角度时涉及一点数学,其余代码基本上都是创建SVG元素然后设置它们属性的DOM代码。
这个示例中最难理解的部分就是绘制每个扇形。用于显示每个户型的元素是< path>元素的d属性来指定。这个属性的值使用字母编码和数值得简略语法,指定了坐标、角度和其他值。比如,字母M表示“move to”(移动到),后面紧跟着x和y坐标。字母L表示“line to”(画线到),即从当前坐标画一条直线到后面紧跟着得坐标点。这个示例也使用了字母A来绘制弧形,后面紧跟得7个数值描述了这个圆弧,如果想了解更多相关细节,可以上网查询相关语法。
示例 15-4:使用JavaScript和SVG绘制饼图
/**
* 创建一个<svg>元素并在其中之一绘制一个饼图
* 这个函数接收一个对象参数,包含下列属性:
* width,height:SVG图形得大小,以像素为单位
* cx,cy,r:饼图得圆心和半径
* lx,ly:图例得左上角坐标
* data:对象,其属性名是数据标签,属性值的对应的值
*
* 这个函数返回一个<svg>元素,调用者必须把它插入文档
* 才可以看到饼图
*/
function pieChart(options){
let {width,height,cx,cy,r,lx,ly,data}=options;
//这是SVG元素的XML命名空间
let svg="http://www.w3.org/2000/svg";
//创建<svg>元素,指定像素大小及用户坐标
let chart=document.createElementNS(svg,"svg");
chart.setAttribute("width",width);
chart.setAttribute("height",height);
chart.setAttribute("viewBox",`0 0 ${width} ${height}`);
//定义饼图的文本样式,如果不在这里设置这些值
//也可以使用CSS来设置
chart.setAttribute("font-family","sans-serif");
chart.setAttribute("font-size","18");
//取得数组形式的标签和值,并计算所有值的总和
//从而找到这张饼到底有多大
let labels=Object.keys(data);
let values=Object.values(data);
let total=values.reduce((x,y)=>x+y);
//计算每个户型的角度。户型i的起始角度为angles[i]
//结束角度为angles[i+1]。这里角度以弧度表示
let angles=[0]; values.forEach((x,i)=>angles.push(angles[i]+x/total*2*Math.PI));
//现在遍历饼图的所有扇形
values.forEach((value,i)=>{
//计算扇形圆弧相接的两点
//下面的公式可以保证角度0为
//十二点方向,正角度时钟增长
let x1=cx+r*Math.sin(angles[i]);
let y1=cy-r*Math.cos(angles[i]);
let x2=cx+r*Math.sin(angles[i+1]);
let y2=cx-r*Math.cos(angles[i+1]);
//这是一个表示角度大于半圆的标志
//它对于SVG弧形绘制组件是必需的
let big=(angles[i+1]-angles[i]>Math.PI)?1:0;
//描述如何绘制饼图中一个扇形的字符串
let path=`M${cx},${cy}`+ //移动到圆心
`L${x1},${y1}`+ //画一条直线到(x1,y1)
`A${r},${r} 0 ${big} 1`+ //画一条半径为r的圆弧
`${x2},${y2}`+ //圆弧终点为(x2,y2)
"Z"; //在(cx,cy)点关闭路径
//计算这个扇形的CSS颜色。这个公式只适合计算的
//15种颜色,因此不要在一个饼图中包含超过15扇形
let color=`hsl(${(i*40)%360},${90-3*i}%,${50+2%i}%)`;
//使用<path>元素描述每个扇形,注意createElementNs()
let slice=document.createElementNS(svg,"path");
//现在设置<path>元素的属性
slice.setAttribute("d",path); //设置当前扇形的路径
slice.setAttribute("fill",color); //设置扇形的颜色
slice.setAttribute("stroke","black"); //设置轮廓线为黑色
slice.setAttribute("stroke-width","1"); //宽度为1 CSS像素
chart.append(slice); //把扇形添加到饼图
//现在为对应的键画一个匹配的小方块
let icon=document.createElementNS(svg,"rect");
icon.setAttribute("x",lx); //定位方块
icon.setAttribute("y",ly+30*i);
icon.setAttribute("width",20); //设置大小
icon.setAttribute("height",20);
icon.setAttribute("fill",color); //填充颜色
icon.setAttribute("stroke","black"); //相同的描边颜色
icon.setAttribute("stroke-width","1");
chart.append(icon); //把图标添加到饼图
//在小方块右侧添加一个标签
let label=document.createElementNS(svg,"text");
label.setAttribute("x",lx+30); //定位文本
label.setAttribute("y",ly+30*i+16);
label.append(`${labels[i]} ${value}`); //把文本添加到标签
chart.append(label); //把标签添加到饼图
});
return chart;
}
document.querySelector("#chart").append(pieChart({
width:640,height:400, //饼图的整体大小
cx:200,cy:200,r:180, //饼图的中心和半径
lx:400,ly:10, //图例的位置
data:{ //要呈现的数据
"JavaScript":71.5,
"Java":45.4,
"Bash/Shell":40.4,
"Python":37.9,
"C#":35.3,
"PHP":31.4,
"C++":22.1,
"TypeScript":18.3,
"Ruby":10.3,
"Swift":8.3,
"Objective-C":7.3,
"Go":7.2,
}
}));
15.8 < canvas>图形
在HTMK文档中,< canvas>图形本身并不可见,它只是创建了一个绘图表并向客户端JavaScript暴露了强大的绘图API。< canvas>API与SVG的主要区别在于使用画布(canvas)绘图要调用方法,而使用SVG创建图形则需要构建XML元素树。这两种绘图手段同样强大,而且可以相互模拟。但从表面上来看,这两种手段迥然不同,又有各自得优缺点。比如,修改SVG图形很简单,可能只需要从描述中删除元素即可。而要从同样的< canvas>图形中删除元素通常需要先擦掉图形再重新绘制。由于画布绘图API是基于JavaScript的,而且相对比较简洁(不像SVG语法那么复杂),因此这本书会详细介绍。
大多数画布绘图API都没有定义在< canvas>元素上,而是定义在通过画布的getContext()方法获得的“绘图上下文”上。调用getContext()时传入“2d”可以得到一个Canvas RenderingContext2D对象,使用它能够在画布上绘制二维图形。
作为Canvas API的一个简单实例,以下HTML文档使用了< canvas>元素和一些JavaScript展示了两个简单的形状:
<p>This is a red square:<canvas id="square" width="10" height="10"></canvas></p>
<p>This is a blue circle:<canvas id="circle" width="10" height="10"></canvas></p>
</body>
<script>
let canvas=document.querySelector("#square"); //取得第一个画布元素
let context=canvas.getContext("2d"); //取得2D绘图上下文
context.fillStyle='#f00'; //设置填充色为红色
context.fillRect(0,0,10,10); //填充一个方块
canvas=document.querySelector("#circle"); //第二个画布元素
context=canvas.getContext("2d"); //取得上下文
context.beginPath(); //开始一个新”路径“
context.arc(5,5,5,0,2*Math.PI,true); //为路径添加一个图形
context.fillStyle='#00f'; //设置蓝色填充色
context.fill(); //填充路径
</script>
我们知道,SVG将复杂图形可以绘制和填充的直线“路径”或曲线。而Canvas API也使用了路径的概念,但它没有使用字母和数字的字符串来描述路径,而是通过一系列方法用来定义路径。比如前面例子中的beginPath()和arc()调用。定义了路径之后,后面的方法调用(如fill()就会操作该路径。上下文对象的各种属性(如fillStyle)用于指定如何执行操作。
15.8.1 路径与多边形
要在画布上画线uoz填充这些线包围的区域,首先需要定义一个路径。路径是一个或多个子路径的序列。而子路径则是两个或多个通过线段(或曲线段)连接起来的点的序列。开始新路径要调用beginPath()方法,而开始定义子路径要调用moveTo()方法。在通过moveTo()建立起子路径的起点后,可以调用lineTo()将该点连接到一个新的点。以下代码定义了一个包含两个线段的路径:
<div style="width: 200px;height: 200px;background-color: yellow">
<canvas id="my_canvas" height="20px" width="20px">
</canvas>
</div>
<script>
let canvas=document.querySelector("#my_canvas");
let c=canvas.getContext("2d");
c.beginPath(); //开始一个新路径
c.moveTo(100,100); //开始一个子路径,起点为(100,100)
c.lineTo(200,200); //用线段连接点(100,100)和(200,200)
c.lineTo(100,200); //用线段连接点(200,200)和(100,200)
c.fill();
c.stroke();
<script>
要绘制(或“描画”)路径中的两条线段,必须效用strock()方法,而要填充这些线段定义的区域,则要调用fill()方法。
以上代码(加上其他设置线宽和填充色的代码),可以产生下列图形。
我们注意到,图15-7定义的子路径是“开放的”。整个路径只包含两条线段,而且终点并未连接到起点,这意味着图中的三角形区域并不是闭合的区域。fill()方法在填充开放路径时,就好像有一条直线连接了子路径与起点一样。这也是为什么以上代码填充的是三角形区域,而描画的只有三角形的两条边。
如果想描画这个三角形的所有边,必须调用closePath()把子路径的终点连接到起点(也可以调用lineTo(100,100),但这样做的结果是得到三条共享起点和终点的线段,路径并没有真正闭合。在用宽线画图时,还是使用classPath()的视觉效果更好)。
关于strock()和fill()还要另外两个地方需要注意。首先,两个方法都作用于当前路径的所有子路径。假设我们在前面的代码中又添加了另一条路径:
c.moveTo(300,100); //在(300,100)开始一条新子路径
c.lineTo(300,200); //画一条垂线到点(300,200)
如果此时调用strock(),则会描画三角形的两条边和一条不相连的垂线。
关于strock()和fill()要注意的第二点是这两个方法都会修改当前路径。换句话说,调用fill()之后再调用strock()路径仍然还在那里。在操作完一条路径后,如果想开始另一条路径,必须调用beginPath()。如果没有调用,则只会在已有路径上添加子路径,最终可能是在重复绘制原来的子路径。
eg:绘制多边形
<div style="width: 1000px;height: 200px;">
<canvas id="my_canvas" height="200px" width="200px">
</canvas>
</div>
<script>
//定义n边的普通多边形,以(x,y)为中心,r为半径
//顶点沿圆形周长间隔相同的距离
//第一个顶点放在正上方,或者放在指定的角度上
function polygon(c,n,x,y,r,angle=0,counterclockwise=false) {
c.moveTo(x+r*Math.sin(angle), //从第一个顶点开始一条新子路径
y-r*Math.cos(angle)); //使用三角函数计算距离
let delta=2*Math.PI/n; //顶点间的角度距离
for(let i=1;i<n;i++){ //对剩下的每个顶点
angle+=counterclockwise?-delta:delta; //调整角度
c.lineTo(x+r*Math.sin(angle), //添加下一个顶点的线
y-r*Math.cos(angle));
}
c.closePath(); //把最后一个顶点连接到第一个顶点
}
//假设只有一个画布,获得其上下文对象以便画图
let c=document.querySelector("canvas").getContext("2d");
//开始一段新路径并添加多边自路径
c.beginPath();
polygon(c,3,50,70,50); //三角形
polygon(c,4,150,60,50,Math.PI/4); //正方形
polygon(c,5,255,55,50); //五边形
polygon(c,6,365,53,50,Math.PI/6); //六边形
polygon(c,4,365,53,20,Math.PI/4,true); //六边形中再画一个小正方形
//设置一些属性控制图形的外观
c.fillStyle="#ccc" //内部浅灰色
c.strokeStyle="#008" //轮廓深蓝色
c.lineWidth=5; //宽度5像素
//现在通过以下调用来绘制所有多边形(每个都在自己的子路径中)
c.fill(); //填充形状
c.stroke(); //描画轮廓
</script>
注意,这个示例绘制了一个包含正方形的六边形。这个正方形和六边形是由独立的子路径组成的,但它们重叠再一起了。每当这时候(或一条子路径与自身交叉时),画布都需要去顶那个区域再路径内部,那个区域再路径外部。为此,画布使用一种被称为“非零环绕规则”的测试来确定这件事。在上面的示例中,之所以形内部没有被填充,是因为正方形和六边形是以相反方向来绘制的。换句话说,六边形的顶点是通过顺时针方向移动的线段连接的,而正方形的顶点是逆时针方向连接的。加入正方形也是顺时针方向绘制的,则调用fill()也会填充正方形的内部区域。
15.8.2 画布大小与坐标
在HTML中通过< canvas>的width和height属性,或者在JavaScript中通过画布对象的width和height属性可以指定画布的大小。画布坐标系的默认原点在画布左上角的(0,0)点。x坐标向右增大,y坐标向下增大。画布中的点可以使用浮点值来指定。
要修改画布大小必须重置画布对象的width属性还是height属性(即使设置为当前值),都会清除画布,擦掉当前路径,重置所有图形属性(包括当前变换和剪切区域)至其最初状态。
在HTML中指定< canvas>的width和height属性会确定画布的实际像素数。每个像素在内存里会分配4个字节,因此如果width和height都是100,则画布在内存中会用40 000个字节来表示10 000个像素。
此外,HTML的width和height属性也指定了画布在屏幕上(以CSS像素)显示的默认大小。如果window.devicePixelRatio是2,则100*100 CSS像素实际上对应40 000个硬件像素。当画布内容绘制到屏幕上时,内存中的10 000个像素需要放大为屏幕上的40 000个物理像素,这意味着你看到的图形会变模糊。
为优化图片质量,不要在HTML中使用width混合height属性设置画布的屏幕大小。而要使用CSS的样式属性width和height来设置画布在屏幕上的预期大小。然后在通过JavaScript绘制前,再将画布对象的width和height属性设置为CSS像素乘以window.devicePixelRatio。仍以前面100100CSS像素大小的画布为例,这样会导致画布显示为100100像素,但内存中会分配200*200像素(即使是这样,圆弧如果放大画布也可能会导致图形模糊或变成马赛克。相对而言,SVG图形在这种情况下则会保存边缘锐利,无论屏幕显示多大或是否缩放)。
15.8.3 图形属性
线条样式
lineWidth属性指定stroke()绘制的线条有多宽,默认值为1。这里要理解,线宽是在调用stroke()的时候由lineWidth确定的,而非在调用lineTo()或其他路径构建的方法时确定的。
lineGap的默认值是平头(butt),lineJoin的默认值是斜接(miter)。不过如果两条线相交的角度很小,斜接会导致相交角拉的非常长,看起来不舒服。如果某个相交角斜接后长度超过线宽一半乘以miterLimit属性,则这个相交角将改为斜切而非斜接(miter)相交。miterLimit的默认值为10。
stroke()方法既可以画虚线、点线,也可以画实线。而画布的图形状态中也有一组数字可以用作“虚线模式”,即通过数字描述画多少像素、忽略多少像素。与其他线条绘制属性不同,虚线要通过setLineDash()和getLineDash()方法而不是一个属性来设置和获取。要指定点虚线模式,可以像下面这样使用setLineDash():
c.setLineDash([18,3,3,3]); //18px虚线、3px空格、3px点、3px空格
最后,lineDashOffset属性指定虚线模式从那里开始绘制,默认值为0.上面示例中设置的虚线模式在绘制封闭路径时会以18像素的虚线开始。但是,如果这里把lineDashOffset设置为21,则该路径将以点开始,后跟空格和虚线。
颜色、模式与渐变
fillStyle和strokeStyle属性指定如何填充和描绘路径。属性名中的“style”通常指颜色,但这些属性也可以用来指定渐变色甚至图片,用以填充或描绘路径(注意,画一条线与填充这条线两端很窄的范围基本上相同,填充和描绘本质上是相同的操作)。
如果想以实色(或半透明色)填充或描绘,只要把这些属性设置为有效的CSS颜色字符串即可。
如果想以渐变色填充(或描绘),需要将fillStyle(或strokeStyle)设置为CanvasGradient对象。这个对象需要调用上下文的createLinearGradient()或createRadialGradient()方法返回。createLinearGradient()方法的参数是定义一条直线的两个点的坐标(不一定水平或垂直),颜色将在这条直线的方向上渐变。createRadialGradient()的参数需要指定两个圆心和半径(这两个圆不一定是同心圆,但通常第一个圆会完全落在第二个圆内部)。小圆内部区域或大圆外部区域将被实色填充,这两个区域中间的部分则以渐变色填充。
创建了表示要填充的画布区域的CanvasGradient对象后,必须调用这个对象的addColorStop()方法定义渐变色。这个方法的第一个参数是一个介于0.0和1.0中间的数值,第二个参数是一个CSS颜色说明。为定义一个简单的渐变色,至少必须调用这个方法两次,但有可能还不止两次。位于0.0处的颜色是渐变的起点,位于1.0处的颜色是渐变的终点。如果要指定更多颜色,这些颜色必须出现在渐变中特定的小数位置。在指定的这些点之间,颜色会平滑地过渡。下面是几个示例:
//画布对角方向地线性渐变(假设画布没有变形)
let bgfade=c.createLinearGradient(0,0,canvas.width,canvas.height);
bgfade.addColorStop(0.0,"#88f"); //左上角开始于浅蓝色
bgfade.addColorStop(1.0,"#fff"); //渐变到右下角的白色
//两个同心圆之间的渐变。中间完全渐变为
//半透明的灰色,再渐变为完全透明
let dount=c.createRadialGradient(300,300,100, 300,300,300);
dount.addColorStop(0.0,"transparent"); //透明
dount.addColorStop(0.7,"rgba(100,100,100,0.9)"); //半透明灰
dount.addColorStop(1.0,"rgba(0,0,0,0)"); //又透明了
理解渐变最重要的一点是它们跟位置紧密相关的。每次创建渐变,都需要为它指定界限。如果想填充这些界限之外的区域,使用的将是定义该渐变两端的某个实色。
除了实色和渐变色,填充和描绘也可以使用图片。为此,需要将fillStyle或strokeStyle设置为上下文对象的createPattern()方法返回的CanvasPattern对象。这个方法的第一个参数应该是< img >或< canvas>元素,其中包含填充或描绘要使用的图片(注意,在这样使用的时候图片和画布并不需要插入文档中)。createPattern()的第二个参数是字符串“repeat”“repeat-x”“repeat-y”或“no-repeat”,用于指定背景图片是否(以及在哪个方向上)重复。
文本样式
font属性指定fillText()和strokeText()方法在绘制文本时使用的字体。这个属性的值应该是一个字符串,语法与CSS的font属性相同。
textAlign属性指定文本的水平对齐方式,相对于传给fillText()或strokeText()的X坐标。合法的值包括start、left、center、right和end。默认值为start,在从左到右的文本中效果与left相同。
textBaseline属性指定文本这对于Y坐标如何垂直对齐。默认值是alphabetic,适合拉丁字母或类似文字。对于汉语或日语,应该使用ideographic。对于(印度很多语言中使用的)梵文及类似文字,可以使用hanging。其他比如top、middle和bottom值纯粹是几何意义上的基线,基于子块的“em方块”。
阴影
上下文对象有4个属性控制阴影的绘制。适当地设置这些属性,可以为绘制的任何线条、区域、文本或图片添加阴影,让它们就像悬浮在画布上方一般。
shadowColor属性指定阴影颜色。默认值是完全透明的黑色,因此除非将这个属性设置为半透明或不透明,否则不会出现阴影。这个属性只能设置为颜色字符串,阴影不支持模式和渐变。使用半透明阴影色可以产生最真实的阴影效果,因此透过阴影可以看到背景。
shadowOffsetX和shadowOffsetY属性指定阴影的X轴和Y轴偏移量。这两个属性的默认值都是0,即阴影将位于绘制内容的正下方,因而不可见。如果给这两个属性正值,阴影会出现在内容下方和右侧。就像屏幕外面左上角有光源照射到画布一样。偏移量越大阴影也越大,绘制内容看起来距离画布表面也“更高”。这些值不受坐标变换影响,即使形状旋转或缩放了,阴影方向和“高度”也会保持不变。
shadowBlur属性指定阴影边缘的模糊程度。默认值0会产生锐利、丝毫不模糊的阴影。这个值越大,模糊越厉害,上线由实现定义。
半透明与合成效果
如果想用半透明设描绘或填充路径,可以使用类似“rgba(…)”这样支持不透明值的CSS颜色语法设置strokeStyle或fillStyle。RGBA中的A代表Alpha,是一个介于0(完全透明)和1之间的值。Canvas API还提供了另一种使用透明色的方式。如果不想分别指定每个颜色的Alpha通道,或者想给不透明的图片或模型添加透明效果,可以设置globalAlpha属性。这样绘制的每个像素的透明度值都会乘上globalAlpha。默认值是1,完全不透明。如果把globalAlpha设置为0,那么绘制的一切都会变成完全透明。
再描绘线条、填充区域、绘制文本或复制图像时,我们通常希望新像素绘制到画布中已存在像素上。如果绘制的是不透明像素,它们会直接替换相应位置上的已有像素。如果绘制的是半透明像素,那么新(“来源”)像素将与老(“目标”)像素组合,从而让老像素会透过新像素可见,可见度取决于新像素的透明度。
这种组合新的(可能半透明)来源像素与已有(可能半透明)目标像素的过程叫作合成。前面描述的合成过程是Canvas API组合像素的默认方式。通过设置globalCompositeOperation属性可以指定合成像素的其他方式。默认值是source-over,即来源像素被绘制在目标像素“上方”,如果来源像素半透明则组合它们。如果把这个属性设置为destination-over,则画布在合成像素时就好像新的来源像素被绘制在已有目标像素下方一样。如果目标像素是半透明或透明的,则部分或全部来源像素的颜色将在最终结果中可见。再有,如果合唱模型为source-atop,那么画布将根据目标像素组合来源像素,结果就是在画布原来完全透明的部分上什么也不会绘制。除此之外,globalCompositeOperation还有其他一些合法的值,但多数只在特殊的场合下才有用,这里就不介绍了。
保持和恢复图形状态
由于Canvas API在上下文对象上定义图形属性,有人可能想多次调用getContext()以获得多个上下文对象。这样一来,或许可以在每个上下文上定义不同的属性。换句话说,每个上下文就像拥有不同的笔刷一样,将以不同的颜色绘制或以不同的宽度画线。遗憾的是,这种做法对画布而言是行不通的。每个< canvas>元素只有一个上下文对象,每次调用getContext()返回的都是同一个CanvasRenderingContext2D对象。
尽管Canvas API一次只允许定义一组图形属性,但它也允许保存当前的图形状态,以便修改其中的属性,之后再恢复。save()方法把当前的图形状态推到一盒保存的状态栈中。restore()方法从该栈中弹出状态,恢复最近一次保存的状态。本节介绍的所有属性都存在于保存的状态中,其中也包括当前的变换及剪切区域(稍后我们将介绍这两个概念)。重要的是,当前定义的路径和当前的点并不属于图形状态,不能保存和恢复。
15.8.4 画布绘制操作
前面介绍了一些基本的画布方法,包括beginPath()、moveTo()、closePath()、fill()和stroke(),可以用来定义,填充、绘制线条和多边形。除此之外,Canvas API还提供其他绘制方法。
矩形
CanvasRenderingContext2D定义了4个绘制矩形的方法。这些方法都接收2个参数,用于指定矩形的一个角和矩形的宽度和高度。正常情况下,都是指定矩形左上角,然后传入正值作为宽度和高度。不过也可以指定其他角,可以传入负值。
fillRect()将以当前fillStyle填充指定的矩形。strokeRect()使用当前strokeStyle和其他线条属性描绘指定矩形的轮廓。clearRect()与fillRect()类似,但它会忽略当前填充样式,直接以(所有空画布默认的)透明黑色像素填充矩阵。这三个方法都不影响当前路径或该路径中的当前点。
最后一个矩形方法是rect(),它影响当前路径。这个方法会将自己拥有的一个矩形子路径添加到当前路径。与其他路径定义方法类似,这个方法本身什么也不会填充或描绘。
曲线
路径有一系列子路径构成,子路径又由一系列相互连接的点构成。在15.8.1节中定义路径时,点和点之间都是通过直线连接的,但实践中并非只需要直线。CanvasRenderingContext2D对象定义了一些方法,用于将一个新点添加到子路径,然后用一条曲线来连接当前点与新点。
arc()
这个方法向路径中添加一个圆形或圆形的一部分(圆弧)。要绘制的弧形通过6个参数指定:圆心的x和y坐标、圆的半径、圆弧的起始和终止角度,以及圆弧在连个角度间的绘制方式(顺时针还是逆时针)。如果路径中有一个当前点,则这个方法用一条直线连接当前点与圆弧的起点(在绘制楔形或扇形时有用),然后用圆形的一部分连接圆弧的起点和终点,最后让圆弧的终点成为新的当前点。如果调用这个方法时没有当前点,则只向路径中添加这条圆弧。
ellipse()
这个方法与arc()方法非常类似,只是会向路径中添加一个椭圆或椭圆形的一部分。另外,这个方法接收两个半径:x轴半径和y轴半径。而且,因为椭圆不是径向对称的,所以这个方法也接收另外一个参数用于指定弧度数,即椭圆绕其圆心顺时针旋转度数。
arcTo()
这个方法会像arc()一样绘制一条直线和一条圆弧,但它使用不同的参数来指定要绘制的圆弧。arcTo()的参数指定点P1和P2,以及一个半径。添加到路径的圆弧具有指定的弧度。起点是以(想象中)当前点到P1点连线为切线的切点,终点是以(想象中)P1到P2点连线为切线的切点。这个看似不同寻常的指定圆弧的方法实际上对绘制有圆角的形状非常有用。如果半径为0,这个方法将只从点到P1绘制一条直线。然而对于非0值半径,它会从当前点朝P1画一条直线,然后围绕一个圆形弯曲这条直线,直至这条线指向P2点。
bezierCurveTo()
这个方法会向子路径中添加一个新点P,并通过一条贝塞尔曲线连接当前点与这个新店。曲线形状通过两个“控制点”C1和C2来指定。在曲线的起点(当前点),曲线朝向C1点方向。在曲线终点(P点),曲线自C2点的方向到达。在这些点之间,曲线平滑变化。点P最终变成子路径新的当前点。
quadraticCurveTo()
这个方法会向子路径中添加一个新点P,并通过一条三次贝塞尔曲线连接当前点与这个新点。曲线形状通过两个“控制点”C1和C2来指定。在曲线的起点(当前点),曲线朝向C1点方向。在曲线终点(P点),曲线自C2点的方向到达。在这些点之间,曲线平滑变化。点P最终变成子路径新的当前点。
示例 15-6: 向路径中添加曲线
<body>
<div style="width: 1200px;height: 200px;">
<canvas id="my_canvas" height="200px" width="1200px">
</canvas>
</div>
</body>
<script>
//将角度转换为弧度的辅助函数
function rads(x) {
return Math.PI*x/180;
}
//取得文档画布元素的上下文对象
let c=document.querySelector("canvas").getContext("2d");
//定义一些图形属性以绘制曲线
c.fillStyle="#aaa" //填充灰色
c.lineWidth=2; //2像素宽的黑(默认)线
//画一个圆形
//没有当前点,因此只绘制圆形,
//没有从当前点到圆形起点的直线
c.beginPath();
c.arc(75,100,50, //圆心位于(75,100),半径50
0,rads(360),false //顺时针从0到360度
);
c.fill(); //填充这个图形
c.stroke(); //描绘其轮廓
//借着以相同方式画一个椭圆形
c.beginPath(); //开启一段新路,不跟圆形连接
c.ellipse(200,100,50,35,rads(15), //圆心、半径和旋转度数
0,rads(360),false); //起始角度、终止角度、方向
//画一个扇形。角度按顺时针从x轴正向度量
//注意arc()会从当前点向弧形起点添加一条线
c.moveTo(325,100); //从圆形的圆心开始
c.arc(325,100,50, //圆心和半径
rads(-60),rads(0), //从-60度开始,转到0度
true); //逆时针
c.closePath(); //再向圆心添加一条线
//类似的扇形,稍微有点偏移,方向相反
c.moveTo(340,92);
c.arc(340,92,42,rads(-60),rads(0),false);
c.closePath();
//使用arcTo()来画圆角。这里绘制一个方形
//其左上角点位于(400,50),各圆角半径不同
c.moveTo(450,50); //从顶点之间开始
c.arcTo(500,50,500,150,30); //添加部分顶点和右上角
c.arcTo(500,150,400,150,20); //添加右边和右下角
c.arcTo(400,150,400,50,10); //添加底边和左下角
c.arcTo(400,50,500,50,0); //添加底边和右下角
c.closePath();
//二次贝塞尔曲线,一个控制点
c.moveTo(525,125); //从这里开始
c.quadraticCurveTo(550,75,625,125); //画曲线到(725,100)
c.fillRect(645-3,70-3,6,6); //标记控制点
c.fillRect(705-3,130-3,6,6);
//三次槽贝塞尔曲线
c.moveTo(625,100); //起点为(625,100)
c.bezierCurveTo(645,70,705,130,725,100); //画曲线到(725,100)
c.fillRect(645-3,70-3,6,6); //标记控制点
c.fillRect(705-3,130-3,6,6);
//最后,填充曲线并描绘其轮廓
c.fill();
c.stroke();
</script>
文本
要在画布中绘制文本,一般都使用fillText()方法,该方法使用fillStyle属性指定的颜色(或渐变、模式)绘制文本。对于大型文本的特效,可以使用strokeText()绘制个别字形的轮廓。这两个方法都以要绘制的文本作为第一个参数,以文本的x和y坐标作为第二和第三个参数。它们都不影响当前路径或当前点。
fillText()和strokeText()还接收可选的第四个参数。如果指定,这个参数用于限制文本可以显示的最大宽度。如果在使用font属性绘制文本时,文本宽度超过了指定的值,为适应这个宽度,画布将缩小问二八年或者使用更窄或更小的字体。
如果想在绘制文本前度量其大小可以将文本传给measureText()方法。这个方法返回一个TextMetrics对象,该对象指定了以当前font属性绘制文本时的度量指标。在本书写作时,TextMetrics对象中包含的唯一“度量指标”是宽度。可以像下面这样查询文本绘制到屏幕时的宽度:
let width=c.measureText(text).width;
知道这个宽度有时候很有用,比如要在画布上居中一段文本。
图片
除了矢量图形(路径、线条等),Canvas API也支持位图图片。drawImage()方法会将一张源图片(或源图片中某个矩形区域)的像素复制到画布上