JavaScript权威指南 第15章 网络编程 第三部分

article/2025/4/20 23:46:55

JavaScript权威指南 第15章 网络编程 第三部分

    • 可伸缩矢量图形
      • 15.7.1 在HTML中使用SVG
      • 15.7.2 编程操作SVG
      • 15.7.3 通过JavaScript创建SVG图片
    • 15.8 < canvas>图形
      • 15.8.1 路径与多边形
      • 15.8.2 画布大小与坐标
      • 15.8.3 图形属性
        • 线条样式
        • 颜色、模式与渐变
        • 文本样式
        • 阴影
        • 半透明与合成效果
        • 保持和恢复图形状态
      • 15.8.4 画布绘制操作
        • 矩形
        • 曲线
          • arc()
          • ellipse()
          • arcTo()
          • bezierCurveTo()
          • quadraticCurveTo()
        • 文本
        • 图片

可伸缩矢量图形

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),半径500,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()方法会将一张源图片(或源图片中某个矩形区域)的像素复制到画布上


http://chatgpt.dhexx.cn/article/8rlX0Igu.shtml

相关文章

python对比c语言_通过实例浅析Python对比C语言的编程思想差异

我一直使用 Python&#xff0c;用它处理各种数据科学项目。 Python 以易用闻名。有编码经验者学习数天就能上手&#xff08;或有效使用它&#xff09;。 听起来很不错&#xff0c;不过&#xff0c;如果你既用 Python&#xff0c;同时也是用其他语言&#xff0c;比如说 C 的话&…

中科数创 php,PHP生成验证码 - 低调是最牛逼的炫耀 - OSCHINA - 中文开源技术交流社区...

PHP提供了一系列函数来实现在网站编程中对图像进行编辑。PHP的图像处理函数都封装在一个函数库中&#xff0c;这就是GD库。GD库用于处理图像&#xff0c;它是一个开放源码的动态创建图像的函数库&#xff0c;可以创建和操作多种不同格式的图像文件&#xff0c;并可以直接以图像…

1+X Web前端等级考证 | PHP 技术与应用(中级重点)

文章目录 动态网站动态网站开发所需构件PHP技术基础php 的诞生php 的优点php 的缺点 开发环境php 语言基础文件命名语言标记注释符与结束符常用命令和系统函数变量与常量常量变量变量与常量的差异 数据类型数据类型转换字符串数组数组转JSON 运算符流程控制流程分支 循环结构wh…

在直播卖货系统中,分销和代销是什么意思?

就在前几天&#xff0c;艾瑞咨询发布了《2020年中国直播电商生态研究报告》&#xff0c;报告中指出&#xff0c;19年至今&#xff0c;直播电商整体成交额达4512.9亿元&#xff0c;同比增长200.4%&#xff0c;占网购整体规模的4.5%&#xff0c;成长空间较大&#xff0c;预计未来…

社交零售多商户分销商城APP小程序系统

多种业务场景模式 自由灵活切换 新零售时代主流多用户商城&#xff0c;满足多种用户形态多样业务场景&#xff0c;解决线上的引流&#xff0c;推广难题&#xff01; 自营模式 1.平台建立自营线上商城&#xff0c;整合自身多渠道业务&#xff0c;通过会员、商品、订单、财务等…

微信小程序、APP分销商城开发:分销功能模块设计

前面我们讲了微信小程序商城基础营销功能&#xff1a;微信小程序商城、APP商城开发营销活动功能策划&#xff08;拼团、砍价、秒杀、直播、优惠券等&#xff09; 今天讲的分销系统更是强大的营销功能&#xff0c;它应该如何设计呢&#xff1f;我们的系统经过一点一点更改更新&…

人人商城源码怎么安装MySQL_人人商城12个常见错误解决方案

人人商城12个常见错误解决方案 微信小程序报错request:fail url not in domain list 有两个原因第一个是报错提示说请求的url不在域名列表里&#xff0c;应该是还没有配置服务器域名&#xff0c;可点击开发者工具右上角 详情-域名信息&#xff0c;看看是否配置了域名&#xff1…

三级分销商城  避坑避雷指南

三级分销自出现以来就引发社会各界的关注&#xff0c;这种病毒式的传播方式产生的盈利效果&#xff0c;让许多商家都蠢蠢欲动&#xff0c;相比于这种模式的风险性&#xff0c;如何去赚钱才是商家最关心的事情&#xff0c;基于三级分销模式研发的三级分销商城也成为企业发展线上…

小程序分销平台商城开发系统

小程序分销平台商城开发系统找何经理。小程序分销商城、小程序分销系统平台等。从更广阔的视角来说,目前移动互联正在给我们的世界带来新的沟通、学习、工作、生活方式,并将引领未来的潮流。不再是单纯的电子渠道建设、业务线上迁移与技术改造升级,而是一场使企业产品创新、…

三级分销系统哪家好

众所周知&#xff0c;如今许多年轻人都纷纷挤进创业这个行列&#xff0c;使得各行各业的商业市场竞争日趋激烈。在这个问题面前&#xff0c;企业如何最大程度的做好营销推广&#xff0c;使销售业绩达到最高值呢&#xff1f;当然&#xff0c;我们不得不承认互联网是最有效果的渠…

PHP商城源码分销奖励/绑定关系设计

来客推PHP分销商城源码是通过互联网将供应商与经销商有机地联系在一起&#xff0c;打造多层级的分销成交平台。 通过分享、邀请等方式绑定上下级关系&#xff0c;把会员和消费者变成分销商&#xff0c;让分销商宣传售卖商品。 分销商获得佣金和奖励&#xff0c;而供货商得到低成…

CRMEB v3.1分销设计思路

1、CRMEB分销模式有几种&#xff0c;有什么区别&#xff1f; 两种分销模式&#xff0c;指定分销和人人分销&#xff1b;指定分销&#xff1a;用户默认无分销权限&#xff0c;需要后台开通分销权限后&#xff0c;才可以推广下级获得返佣&#xff1b;人人分销&#xff1a;用户默…

人人商城小程序消息服务器配置,微擎系统人人商城小程序前端配置教程

人人商城是目前微信商城系统里面用的最多的程序&#xff0c;它功能强大&#xff0c;支持多商户入驻、分销功能、拼团活动、秒杀活动、直播购物功能等。现在更是支持了微信小程序功能&#xff0c;那么人人小程序是如何安装的呢。 首先你得准备认证服务号、服务器、备案域名。人人…

直播教育平台源码中的人人分销是什么?如何实现?

根据艾瑞咨询发布的《2020年H1中国教育行业广告主营销策略研究报告》显示&#xff0c;近段时间&#xff0c;在线教育的市场投放成本节节高升。对于刚刚崛起的在线教育来说&#xff0c;如何低成本获客就成为了一个非常重要的问题。其中&#xff0c;在直播教育平台源码中加入人人…

免费分销工具—运营指南分销系统

免费分销工具—运营指南分销系统 疫情之下,无人出门到陆续复工,餐饮、教培、旅游等实体行业行业到店消费人数并未猛然上涨,所谓的“报复性消费”仍未到来,实体行业店面收入远远低于成本,经营极度困难。 在这种困境下,如何增加订单,获取更多顾客,实现逆势增长? 01 …

公排系统php,全球公排自动滑落二二复制多级分销系统PHP二二复制多级分销源码...

菜鸟源码网分享全球公排自动滑落二二复制多级分销系统PHP二二复制多级分销源码&#xff0c;站长测试源码安装和后台登陆功能设置保存正常&#xff0c;用户端注册登录购买商品正常&#xff0c;商户端登陆添加商品等正常。PHP全球公排自动滑落 二二复制 九九复制带加权分红系统&a…

火遍全网的全民分销系统,你了解多少?

随着互联网的发展&#xff0c;用户流量也逐渐壮大&#xff0c;对于企业来说&#xff0c;平台发展需要流量&#xff0c;怎么样获取流量一个重点问题&#xff0c;随着电商的深入发展&#xff0c;人们探索出各种更深层次挖掘流量的办法&#xff0c;全民分销系统就是其中一种&#…

公排系统php,全球公排自动滑落二二复制多级分销系统 PHP源码

详细制度讲解 1、分销与公排并行1&#xff0d;9级自定义设置分销层级 2&#xff0d;9级自定义递增排位公排&#xff1a;二二复制、三三复制………九九复制 2、威 信分销的状态可以开启或关闭&#xff0c;由您说了算(关闭分销后将不再启用分销和公排) 公排的状态可以开启或关闭(…

php商城开发人人分销团队级差分红升级规则订制

注&#xff1a;所有内容可定制开发&#xff0c;可单独模块更新&#xff0c;联系QQ:1002424826&#xff0c;微信&#xff1a;php510666 使用须知 1.如果您已经在使用本系统&#xff0c;即表示您已经同意以下免责声明条款&#xff0c;否则请立即停止使用本系统。 2.使用方在使用…

线上连锁线下整合的连锁电商架构 打造店店互推人人分销模式

传统的连锁店需要上线电商平台&#xff0c;这是一个十分困难的事情&#xff0c;因为原来的门店会觉得上线的电商会损害原来的利益&#xff0c;加上不少门店没有任何线上运营的基础&#xff0c;对线上网店十分排斥。核货宝小编认为&#xff0c;如何将线下连锁与线上业务打通&…