送你一个月亮和笑脸 | HTML5图形开发 从最基础开始学Canvas(一):路径及基本图形绘制(线条、矩形和圆弧)

911 阅读7分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

svg标签,是html5中矢量图的应用;

canvas标签,是html5中位图的应用。

Cnavas标签

如下为一个canvas标签的基本结构,以下所有示例均在此基础上完成(重复代码部分不再在写出)

<canvas id="c1" width="600" height="500"></canvas>
<script>
    let canvas=document.getElementById("c1");    
    let ctx=canvas.getContext("2d");   

    // 示例代码...
</script> 

Canvas坐标系统

canvas基于栅格(grid)和坐标空间完成图形的定位和长度的度量。

如图,canvas元素默认被网格所覆盖。通常来说网格中的一个单元相当于canvas元素中的一像素。栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点来定位

依据坐标原点平移、网格旋转以及缩放。

图中蓝色方形左上角的坐标为距离左边(X轴)x像素,距离上边(Y轴)y像素。(坐标为(x,y))。

Canvas如何绘制图形?

canvas图形的绘制分成两部分:

  1. 骨骼,图形的架子,也叫路径path。使用moveTo、lineTo等指定绘制的点、线坐标。

  2. 绘制,给定颜色进行描绘。绘制分两种方式:

    1. 描边
      • 1.1 描边样式:描边颜色 ctx.strokeStyle="red"; 描边粗细 ctx.lineWidth=2;
      • 1.2 执行绘制,即绘制线条 ctx.stroke();
    2. 填充
      • 2.1 填充颜色 ctx.fillStyle="blue";
      • 2.2 执行绘制,即实现填充 ctx.fill()

由此描边和填充执行的先后顺序不同,将会导致一些不同的显示结果,如下图所示

canvas中的绘图是基于状态的绘图,即先设置一个绘图的状态,然后再调用具体的函数进行绘制。

如画一条直线:

// 绘制一个20,20到120,120的直线
ctx.moveTo(20,20);
ctx.lineTo(120,120);

// 调用stroke()进行具体的绘制
ctx.stroke();

路径绘制

一个路径,甚至一个子路径,都是闭合的。

使用路径绘制图形的步骤:

  1. 创建路径起始点。

beginPath()新建一条路径,moveTo(x, y) 设置路径起始点。

绘制路径前要显式调用beginPath()方法

  1. 调用绘制方法去绘制出路径

lineTo(x,y)绘制一条从当前路径到指定坐标的直线。

  1. 把路径封闭

closePath()闭合路径之后,图形绘制命令又重新指向到canvas画布的上下文中。

(实际使用中常常省略,确保必要时新开始一条路径即可。比如fill()填充会自动闭合路径)。

  1. 一旦路径生成,就可以通过描边或填充路径区域来渲染图形。
  • stroke() 用线条来绘制图形轮廓。
  • fill() 填充路径的内容区域生成实心的图形。

line线条及属性

line线条

绘制直线组合成三角形,并设置边框和填充

  • 三角形
// 直线 三角形 边框样式
ctx.beginPath()
ctx.moveTo(10,180);
ctx.lineTo(300,20);
ctx.lineTo(400,220);
ctx.closePath(); // closePath会闭合路径,形成三角形。取消闭合 为两条线段
ctx.strokeStyle="green";// 线条颜色 .默认黑色
ctx.stroke(); 

  • 实心三角形
// 填充的三角形
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.lineTo(200, 200);

ctx.fillStyle="red"; //填充的颜色
ctx.fill(); //填充闭合区域。如果path没有闭合,则fill()会自动闭合路径。

绘制多边形

随意绘制一个闭合的多边形

// 多个线条绘制多边形
ctx.beginPath();
ctx.moveTo(20,30);
ctx.lineTo(300,20);
ctx.lineTo(350,200);
ctx.lineTo(280,330);
ctx.lineTo(70,290);
// ctx.closePath(); 
ctx.strokeStyle="red";
ctx.stroke();

line线条属性

lineCap——线段末端样式

  • round: 线段末端增加一段圆形结束
  • butt (默认值)线条末端方形结束
  • square 线段末端增加一个(宽度和线段相同,高度是线段厚度(宽度)一半)方形结束
//    
    ctx.beginPath();
    ctx.moveTo(50,50);
    ctx.lineTo(50,300);
    ctx.lineWidth=20;
    ctx.strokeStyle="red";
    ctx.lineCap="butt";// 默认
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(100,50);
    ctx.lineTo(100,300);
    ctx.lineWidth = 20;
    ctx.strokeStyle = "red";
    ctx.lineCap="round";
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(150, 50);
    ctx.lineTo(150, 300);
    ctx.lineWidth = 20;
    ctx.strokeStyle = "red";
    ctx.lineCap = "square";
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(0, 49);
    ctx.lineTo(300, 49);
    ctx.moveTo(0, 301);
    ctx.lineTo(300, 301);
    ctx.lineWidth = 0.5;
    ctx.strokeStyle = "blue";
    ctx.stroke();

线条末端的样式

lineJoin——线条与线条接合处的样式

同一个 path 内,设定线条与线条间接合处的样式。

  1. round 通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度。

  2. bevel 在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角。

  3. miter(默认) 通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。

// linejoin()
    var lineJoin = ['round', 'bevel', 'miter'];
    ctx.lineWidth = 20;
    ctx.strokeStyle = "red";
 
    for (var i = 0; i < lineJoin.length; i++){
        ctx.lineJoin = lineJoin[i];
        ctx.beginPath();
        ctx.moveTo(50, 50 + i * 50);
        ctx.lineTo(100, 100 + i * 50);
        ctx.lineTo(150, 50 + i * 50);
        ctx.lineTo(200, 100 + i * 50);
        ctx.lineTo(250, 50 + i * 50);
        ctx.stroke();
    }

虚线——setLineDash()和lineDashOffset

setLineDash() 方法和 lineDashOffset 属性实现虚线样式。

  • setLineDash 方法接受一个数组,来指定线段与间隙的交替;
  • lineDashOffsetc属性设置起始偏移量。
//虚线
ctx.setLineDash([20, 5]);  // [实线长度, 间隙长度]
ctx.lineDashOffset = -0;
ctx.strokeRect(50, 50, 210, 210);

填充和描边顺序不同导致结果不同

// 先填充后描边和先描边后填充
    ctx.beginPath();
    ctx.moveTo(30, 40);
    ctx.lineTo(200, 30);
    ctx.lineTo(200, 200);
    ctx.closePath();
    ctx.fillStyle = "blue";
    ctx.fill();
    ctx.strokeStyle="red";
    ctx.lineWidth=20;
    ctx.stroke();
    
    
    ctx.beginPath();
    ctx.moveTo(230, 40);
    ctx.lineTo(400, 30);
    ctx.lineTo(400, 200);    
    ctx.closePath();
    ctx.strokeStyle="red";
    ctx.lineWidth=20;
    ctx.stroke();
    ctx.fillStyle="blue";
    ctx.fill();

矩形——直接绘制图形

<canvas>只支持一种原生的 图形绘制:矩形。所有其他图形都至少需要生成一种路径(path)。

矩形相关的方法有fillRectstrokeRectclearRect,其具体效果如下:

// 绘制矩形
//ctx.fillStyle="red";//#f00
ctx.fillStyle = "rgba(0, 0, 200, 0.3)";
ctx.fillRect(10,10,50,50);// 绘制实心的 x,y,width,height 的矩形
ctx.strokeRect(10, 60, 50, 50);// 绘制 x,y,width,height 的矩形边框
ctx.clearRect(20, 20, 30, 30);// 清除指定的矩形区域  完全透明。

圆弧

绘制圆弧有两种方法:arc()arcTo()

  • arc(x, y, r, startAngle, endAngle, anticlockwise): 以(x, y) 为圆心,以r 为半径,从 startAngle 弧度开始到endAngle弧度结束。anticlosewise:true——逆时针,false——顺时针(默认顺时针)。
  • arcTo(x1, y1, x2, y2, radius): 根据给定的控制点和半径画一段圆弧,最后再以直线连接两个控制点。

角度和弧度

Math.PI=180度

angle=(Math.PI/180)*degrees //角度转弧度

degrees=(180/Math.PI)*angle //弧度转角度

arc绘制圆弧

  • 圆弧或填充圆弧
    let degreeToAngle = function (degree) {
        return Math.PI / 180 * degree;
    }

    // arc 圆弧 
    let width=canvas.width,height=canvas.height;
    // 圆点
    let origin={
        x:width/2,
        y:height/2
    }

    // 圆弧 及圆弧填充
    ctx.beginPath();
    ctx.arc(origin.x,origin.y,100, degreeToAngle(0),degreeToAngle(70));
    ctx.strokeStyle="blue";
    ctx.lineWidth=4;
    ctx.stroke();
    ctx.fillStyle="red";
    ctx.fill();

  • 填充扇形或圆
    // 填充扇形
    ctx.beginPath();
    ctx.moveTo(origin.x, origin.y); //起始位于圆心
    // ctx.arc(origin.x,origin.y,100,degreeToAngle(0),degreeToAngle(110));    
    ctx.arc(origin.x, origin.y, 100, degreeToAngle(0), degreeToAngle(360));    
    ctx.closePath();  // 闭合路径 扇形边
    ctx.lineWidth=6;
    ctx.strokeStyle="blue";
    ctx.stroke();
    ctx.fillStyle="red";
    ctx.fill();

arcTo绘制圆弧

// 路径当前位置为起始点
// x1, y1 为控制点1
// x2, y2 为控制点2
arcTo(x1, y1, x2, y2, r)

arcTo中控制点的说明如下图:

arcTo()可以理解为:绘制的弧形是由两条切线所决定(与两条直线相切,但由于半径参数,所以不会是真正的圆形)。

第 1 条切线:起始点和控制点1决定的直线。

第 2 条切线:控制点1 和控制点2决定的直线。

ctx.beginPath();
// 起始点
ctx.moveTo(50, 50);
//参数1、2:控制点1坐标   参数3、4:控制点2坐标  参数4:圆弧半径
ctx.arcTo(200, 50, 200, 200, 100);
ctx.lineTo(200, 200); // 如果不画lineTo, 圆弧未必会画到控制点2 就结束 
ctx.stroke();

ctx.beginPath();
ctx.rect(50, 50, 10, 10);
ctx.rect(200, 50, 10, 10);
ctx.rect(200, 200, 10, 10);
ctx.fill();

Transparency(透明度)设置

rgba颜色设置透明度

设置指定线条或填充的透明度

ctx.beginPath();
ctx.rect(20, 20, 100, 80);
ctx.strokeStyle = "rgba(0,255,0,.3)";
ctx.lineWidth = 20;
ctx.stroke();
ctx.fill();

globalAlpha全局透明度

globalAlpha = transparencyValue: 设置 canvas 所有图形的透明度,有效值 0.0(完全透明)到 1.0(完全不透明),默认是 1.0。

ctx.beginPath();
ctx.rect(20,20,100,80);
ctx.strokeStyle="rgba(0,255,0,1)"
ctx.globalAlpha = 0.3;
ctx.lineWidth=20;
ctx.stroke();
ctx.fill();

综合示例

笑脸

    let width=canvas.width,height=canvas.height;
    // 圆点
    let origin={
        x:width/2,
        y:height/2
    }

    // 笑脸
    let face_radius=100;
    ctx.beginPath();
    ctx.arc(origin.x,origin.y, face_radius,0,2*Math.PI);
    ctx.lineWidth = 2;
    ctx.stroke();

    //眉毛和眼睛
    let per_dist= face_radius* 2 / 3;
    let leftEyeOrigin={
        x: origin.x - per_dist / 2,
        y: origin.y - per_dist / 3
    }

    // 眉毛
    ctx.beginPath();
    ctx.arc(leftEyeOrigin.x, leftEyeOrigin.y, face_radius/3,degreeToAngle(225),degreeToAngle(315));    
    ctx.lineWidth = 2;
    ctx.stroke();

    let rightEyeOrigin = {
        x: origin.x + per_dist / 2,
        y: origin.y - per_dist / 3
    }
    ctx.beginPath();
    ctx.arc(rightEyeOrigin.x, rightEyeOrigin.y, face_radius / 3, degreeToAngle(225), degreeToAngle(315));
    ctx.lineWidth = 2;
    ctx.stroke();

    // 眼睛
    ctx.beginPath();
    ctx.arc(leftEyeOrigin.x,leftEyeOrigin.y,face_radius/5,0,2*Math.PI);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(leftEyeOrigin.x, leftEyeOrigin.y, face_radius / 10, 0, 2 * Math.PI);
    ctx.fillStyle = "#7b5028";
    ctx.fill();

    ctx.beginPath();
    ctx.arc(rightEyeOrigin.x, rightEyeOrigin.y, face_radius / 5, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(rightEyeOrigin.x, rightEyeOrigin.y, face_radius / 10, 0, 2 * Math.PI);
    ctx.fillStyle="#7b5028";
    ctx.fill();

    // mouth
    mouth={
        x:origin.x,
        y:origin.y+ face_radius/10
    } 
    ctx.beginPath();
    ctx.arc(mouth.x,mouth.y,face_radius/2,degreeToAngle(30),degreeToAngle(150));
    ctx.stroke();

月亮

    let width=canvas.width,height=canvas.height;
    // 圆点
    let origin={
        x:width/2,
        y:height/2
    }

    // 填充整个画布
    ctx.fillStyle="#000";
    ctx.fillRect(0,0,canvas.width,canvas.height);
    // 月亮
    let earth_radius=100;
    let moon_radius=80;
    let moon_origin={
        x:origin.x-60,
        y:origin.y
    }
    
    ctx.beginPath();
    ctx.arc(moon_origin.x,moon_origin.y,moon_radius,0,2*Math.PI);
    ctx.fillStyle="yellow";
    ctx.fill();
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(origin.x, origin.y, earth_radius, 0, 2 * Math.PI);
    ctx.fillStyle="#000";
    ctx.fill();
    ctx.stroke();