• Canvas学习:封装Canvas绘制基本图形API


    Canvas学习:封装Canvas绘制基本图形API

     

    从前面的文章中我们了解到,通过Canvas中的CanvasRenderingContext2D对象中的属性和方法,我们可以很轻松的绘制出一些基本图形,比如直线、弧线、矩形、圆形、三角形等。但有很多基本图形的绘制是没有现成的方法,需要通过CanvasRenderingContext2D对象中的属性和方法组合在一起才能绘制出来,比如说点划线、箭头和正多边形等。为了更好的帮助大家在Canvas中绘制这些基本图形,可以将这些基本图形的绘制封装起来。今天这篇文章,我们主要来看看怎么将这些函数封装。

    回忆前面的内容

    Canvas学习系列到目前为止已整理的都是关于基本图形绘制相关的知识,主要涵盖:

    如果您和我一样初次接触Canvas,建议您先花一定的时间阅读上面这些文章,有助于您更好的理解下面的内容。

    Canvas绘图相关属性和方法

    <canvas>元素有一个getContext()方法,这个方法可以用来获取上下文和它的绘画功能。getContext()只有一个参数,上下文的格式。我们目前学习的上下文环境都是一个2D环境,所以我们所说的也是对于2D图像而言。它具CanvasRenderingContext2D对象。这个对象包括一些绘制图形方法和设置图形样式的属性。

    状态

    CanvasRenderingContext2D渲染环境包含了多种绘图的样式状态(属性有线的样式、填充样式、阴影样式、文本样式等),其中该对象提供了两个方法,能帮助我们更好的使用好这些状态:

    • CanvasRenderingContext2D.save():使用栈保存当前的绘画样式状态,你可以使用 CanvasRenderingContext2D.restore() 恢复任何改变
    • CanvasRenderingContext2D.restore():恢复到最近的绘制样式状态,此状态是通过 CanvasRenderingContext2D.save() 保存到”状态栈“中最新的元素

    有关于这方面的详细介绍可以阅读Canvas状态:save()restore() 一文。

    变换

    CanvasRenderingContext2D渲染背景中的对象会有一个当前的变换矩阵,一些方法可以对其进行控制。当创建当前的默认路径、绘制文本、图形等会应用此变换矩阵。

    • CanvasRenderingContext2D.rotate(rad):在变换矩阵中增加旋转,角度变量表示一个顺时针旋转角度,并且用弧度表示
    • CanvasRenderingContext2D.scale(sx, sy):根据sx水平方向和sy垂直方向,为Canvas单位添加缩放变换
    • CanvasRenderingContext2D.translate(dx,dy):通过在网格中移动Canvas和Canvas原点dx水平方向、原点dy垂直方向,添加平移变换
    • CanvasRenderingContext2D.transform(a, b, c, d, e, f):使用方法参数描述的矩阵多次叠加当前的变换矩阵
    • CanvasRenderingContext2D.setTransform(a, b, c, d, e, f):重新设置当前的变换为单位矩阵,并使用同样的变量调用CanvasRenderingContext2D.transform(a, b, c, d, e, f)方法
    • CanvasRenderingContext2D.resetTransform():使用单位矩阵重新设置当前的变换

    有关于Canvas中的变换涉及到了Canvas的坐标系统相关知识,建议您阅读前面介绍的Canvas里的坐标系统Canvas坐标变换Canvas自定义的坐标变换三篇文章。

    线型

    CanvasRenderingContext2D提供了相关的方法和属性控制如何在Cavnas画布中绘制线的样式风格:

    • CanvasRenderingContext2D.lineWidth:线的宽度,默认值1.0
    • CanvasRenderingContext2D.lineCap:线末端的类型。允许的值:butt(默认值)、roundsquare
    • CanvasRenderingContext2D.lineJoin:定义两线相交拐点的类型。允许值:miter(默认值)、roundbevel
    • CanvasRenderingContext2D.miterLimit:斜接面限制比例,默认10

    有关于这几个属性的详细介绍,可以阅读Canvas绘制线段Canvas线型

    填充和描边

    填充和描边有对应的属性和方法。其中属性主要用于填充设计用于图形内部的颜色和样式,描边设计用于图形的边线;方法主要用于填充路径和描边路径:

    • CanvasRenderingContext2D.fillStyle:图形内部的颜色和样式(填充),默认#000
    • CanvasRenderingContext2D.strokeStyle:图形边线的颜色和样式(描边),默认#000
    • CanvasRenderingContext2D.fill():使用当前的样式填充子路径
    • CanvasRenderingContext2D.stroke():使用当前的样式描边子路径

    路径

    Canvas的CanvasRenderingContext2D对象中用于操作路径的方法主要有:

    • CanvasRenderingContext2D.beginPath():清空子路径列表开始一个新的路径。当你想创建一个新的路径时,调用此方法
    • CanvasRenderingContext2D.closePath():使笔点返回到当前子路径的起始点。它尝试从当前点到起点绘制一条直线。如果图形已经是封装的或者只有一个点,那么此方法不会做任何操作
    • CanvasRenderingContext2D.moveTo(x, y):将一个新的子路径的起始点移动到(x, y)坐标
    • CanvasRenderingContext2D.lineTo(x, y):使用直线连接子路径的最后的点到(x, y)坐标
    • CanvasRenderingContext2D.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y):添加一个三次贝塞尔曲线路径。该方法需要三个点。 第一、第二个点是控制点,第三个点是结束点。起始点是当前路径的最后一个点,绘制贝赛尔曲线前,可以通过调用 moveTo() 进行修改
    • CanvasRenderingContext2D.quadraticCurveTo(cpx, cpy, x, y):添加一个二次贝塞尔曲线路径
    • CanvasRenderingContext2D.arc(x, y, r, startAngle, endAngle, [anticlockwise]):绘制一段圆弧路径,圆弧路径的圆心在(x,y)位置,圆弧的半径为r,根据anticlockwise指定圆弧的方向从startAngle开始绘制,到endAngle结束(也就是旋转方向,默认为顺时针)
    • CanvasRenderingContext2D.arcTo(x1, y1, x2, y2, radius):根据控制点和半径绘制圆弧路径,使用直线连接前一个点
    • CanvasRenderingContext2D.rect(x,y,width,height):创建一个矩形路径,矩形的起始点位置是(x, y),尺寸为widthheight

    这些路径方法可以帮助我们在Canvas中绘制直线、曲线、弧线,贝塞尔曲线、圆、矩形,甚至结合这些路径方法,可以绘制出其他的图形,比如矩形、多边形,三角形或者其他的复杂图形。这些方法也是绘制图形的基本方法,也是核心方法,只有更好的掌握这些方法,才能更好的在Canvas中绘制出你自己想要的图形。

    有关于这些方法的使用,在下面这些文章都有详细介绍过:

    绘制矩形

    在Canvas中除了可以使用CanvasRenderingContext2D对象中路径方法绘制之外,还专门提供了几个方法来绘制矩形:

    • CanvasRenderingContext2D.clearRect(x, y, width, height):设置指定矩形区域内(以(x,y)为起点,范围是(width, height))所有像素变成透明,并擦除之前绘制的所有内容
    • CanvasRenderingContext2D.fillRect(x, y, width, height): 绘制填充矩形,矩形的起点在(x, y)位置,矩形的尺寸是widthheight
    • CanvasRenderingContext2D.strokeRect(x, y, width, height):在Canvas中,使用当前的绘画样式,描绘一个起点在(x, y)位置,尺寸为widthheight的矩形

    前面我们专门花了一节的内容:Canvas绘制矩形来介绍这几个方法的使用。

    封装绘图的API

    虽然Canvas中的CanvasRenderingContext2D对象有很多方法和属性能帮助我们绘不同的图形,但如果你的工作每天都跟图形打交道的话,建议你使用这些方法和属性封装出绘图的函数或者方法。在接下来的内容我们来看看怎么封装绘制基本图形的函数。

    首先回想一下,我们常常碰到的基本图形有:线段(分别,实现、虚线和圆点线)、箭头、弧线、圆、矩形、扇形和正多边形等。那下面的内容就是来看看怎么写代码。

    声明环境

    在Canvas中都有一个2D的绘图环境,那么我们可以简单的封装一个initDrawCanvas()函数来处理:

    // 声明Canvas对象
    var canvas;
    // 声明Context对象
    function initDrawCanvas(canvas, ctx) {
        this.canvas = canvas;
        this.ctx = ctx;
    }
    

    线段

    线段我们主要常看到的有实线(Solid)、虚线(Dashed)和圆点线(Dotted)。在学习几何知识时,我们知道,两点确定一条线段。那么在我们封装的函数中,我们需要两个点的坐标,比如起始点坐标(startX, startY)和结束点坐标(endX, endY)。另外为了更好的通过封装的函数控制绘制的线段,我们还需要设置线段的宽度和颜色,也就是需要另外两个参数,比如使用lineWidth来传线宽,color传线段的颜色。

    前面也说了,线段分为三种,也就是说我们封装的函数也封装成三个,比如:

    • 实线线段:drawSolidLine()
    • 虚线线段:drawDashedLine()
    • 圆点线段:drawDottedLine()

    通过前面的知识,我们可以使用moveTo()lineTo()两个方法来控制线段的起点和终点,另外lineWidthfillStyle(或者strokeStyle)控制线段粗线和颜色。如此一来,我们可以这样来写drawSolidLine()函数:

    // 绘制实线线段
    // @param {Number} startX - 线段起点x轴坐标
    // @param {Number} startY - 线段起点y轴坐标
    // @param {Number} endX - 线段终点x轴坐标
    // @param {Number} endY - 线段终点y轴坐标
    // @param {Number} lineWidth - 线宽
    // @param {String} color - 线颜色
    function drawSolidLine(startX, startY, endX, endY, lineWidth, color){
        ctx.save();
        ctx.strokeStyle = color;
        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.moveTo(startX, startY);
        ctx.lineTo(endX, endY);
        ctx.stroke();
        ctx.restore();
    }
    

    对于虚线的绘制,在Canvas的对象中提供了一个setLineDash()方法,在这个方法中,我们可以传递一个数组,来控制虚线的间距和长度。在drawSolidLine()基础上我们进行一下扩展,来封装drawDashedLine()函数:

    // 绘制虚线
    // @param {Number} startX - 线段起点x轴坐标
    // @param {Number} startY - 线段起点y轴坐标
    // @param {Number} endX - 线段终点x轴坐标
    // @param {Number} endY - 线段终点y轴坐标
    // @param {Array} setLineDash - 点划线间距
    // @param {Number} lineWidth - 线宽
    // @param {String} color - 线段颜色
    function drawDashedLine(startX, startY, endX, endY, setLineDash, lineWidth, color) {
        ctx.save();
        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.setLineDash(setLineDash);               
        ctx.moveTo(startX, startY);
        ctx.lineTo(endX, endY);
        ctx.closePath();
        ctx.stroke();
        ctx.restore();
    } 
    

    对于圆点线绘制函数相对而言要较为复杂一点,因为在Canvas中没有提供一个类似setLineDash()方法,让我们来控制圆点的圆点大小以及间距。既然我们绘制的是圆点线,那么在Canvas中我们可以使用.arc()方法来绘制圆。如此一来就好办了:

    // 绘制圆点线
    // @param {Number} startX - 线段起点x轴坐标
    // @param {Number} startY - 线段起点y轴坐标
    // @param {Number} endX - 线段终点x轴坐标
    // @param {Number} endY - 线段终点y轴坐标
    // @param {Number} interval - 间隔
    // @param {Number} radius - 圆点半径
    // @param {String} color - 线段颜色
    function drawDottedLine(startX, startY, endX, endY, radius, interval, color) {
        if (!interval) {
            interval = 5;
        }
    
        var isHorizontal = true;
    
        if (startX == endX) {
            isHorizontal = false;
        }
    
        var len = isHorizontal ? endX - startX : endY - startY;
    
        ctx.strokeStyle = color;
        ctx.fillStyle = color;
    
        ctx.save();
        ctx.beginPath();
    
        ctx.moveTo(startX, startY);
    
        var progress = 0;
    
        while (len > progress) {
            progress += interval;
    
            if (progress > len) {
                progress = len;
            }
    
            if (isHorizontal) {
                ctx.moveTo(startX + progress, startY);
                ctx.arc(startX + progress, startY, radius, 0, Math.PI * 2, true);
                ctx.fill();
            } else {
                ctx.moveTo(startX, endX + progress);
                ctx.arc(startX, startY + progress, radius, 0, Math.PI * 2, true);
                ctx.fill();
            }
        }
        ctx.restore();
    }
    

    通过前面的知识,我们可以通过lineJoinlineCap控制线型,但在上面的函数封装中,并没有做这方面相关的考虑。不过并不要紧,在实际使用可以通过ctx.lineJoinctx.lineCap来设置,当然你也可以修改上面的函数,将这个参数传进去。

    矩形

    在Canvas中可能通过rect()路径的绘制,也可以通过fillRect()strokeRect()绘制矩形。使用这些方法绘制矩形都有相同的参数:

    • (x,y):矩形起点坐标,也就是矩形左上角的坐标
    • width:矩形的宽度
    • height:矩形的高度

    在绘制矩形我们有填充和描边矩形之分,另外在Canvas中可以直接使用fillRect()strokeRect()来绘制。只不过Canvas中自带的方法只能绘制直角矩形,如果我们要绘制圆角矩形,那还是需要借用arcTo()方法来制作圆角。为了更好的区分直角矩形和圆角矩形,我们各自为他们封装了一个函数:

    • drawRect():直角矩形
    • drawRoundedRect():圆角矩形

    下面代码是各自函数对应的:

    // 封装直角矩形
    // 矩形包括: 填充矩形、边框矩形和清除矩形区域
    // @param {Number} x - 矩形起点的x坐标
    // @param {Number} y - 矩形起点的y坐标
    // @param {Number} width - 矩形宽度
    // @param {Number} height - 矩形高度
    // @param {Boolean} isClear - 是否绘制清除画布的矩形区域; true则是绘制一个清除画布矩形区域, false就是绘制其他两种矩形
    // @param {Boolean} isFill - 是否填充;true绘制填充矩形, false绘制边框矩形
    // @param {String} color - 矩形颜色
    function drawRect(x, y, width, height, isClear, isFill, color) {
        // 为true表示绘制清除画布的矩形区域,那么传入的isFill,color值可以为任意值
        if (isClear) {
            ctx.clearRect(x, y, width, height);
        } else {
            if (isFill) {
                ctx.fillStyle = color;
                ctx.fillRect(x, y, width, height);
            } else {
                ctx.strokeStyle = color;
                ctx.strokeRect(x, y, width, height);
            }
        }
    }
    

    其中isClear是一个布尔值,用来判断是否要绘制一个清除矩形区域,功能对应的是Canvas中的clearRect()方法。而isFill也是一个布尔值,用来判断是否绘制一个填充矩形还是描边矩形,如果值为true调用fillRect()绘制一个填充矩形,false则调用strokeRect()绘制一个描边矩形。最后传了一个color参数,用来控制矩形的填充颜色或者描边颜色。

    从上面的代码中可以看出,绘制描边矩形时并没有设置边框的粗线,如果你绘制一个描边矩形时,需要设置边框粗组时,在实际调用时,可以借用ctx.lineWidth属性来设置。

    注:drawRect()函数只能绘制填充或描边直角矩形,如果你需要绘制具有填充和描边的一个矩形时,上面的函数就无能为力了,当然你可以通过其他的方法来进行封装,这里就不做过多的阐述了。有兴趣的同学可以自己动手,比如封装一个xxx函数。

    上面是封装绘制直角矩形的函数,接下来看圆角矩形的函数的封装。大致方法是类似的,只不过我们封装圆角矩形时,使用了arcTo()方法来实现圆角,而这个圆角弧度需要一个半径,所以在上面的直角矩形基础上再传一个radius参数:

    // 绘制圆角矩形
    // @param {Number} x - 矩形左上角x轴坐标
    // @param {Number} y - 矩形左上角y轴坐标
    // @param {Number} width - 矩形的宽度
    // @param {Number} height - 矩形的高度
    // @param {Number} radius - 矩形圆角的半径
    // @param {Boolean} isFill - 是否绘制填充,true填充,false边框
    // @param {String} color - 矩形的颜色
    function drawRoundedRect(x, y, width, height, radius, isFill, color) {
        ctx.save();
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.arcTo(x + width, y, x + width, y + radius, radius);
        ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
        ctx.arcTo(x, y + height, x, y + height - radius, radius);
        ctx.arcTo(x, y, x + radius, y, radius);
        ctx.closePath();
    
        if (isFill) {
            ctx.fillStyle = color;
            ctx.fill();
        } else {
            ctx.strokeStyle = color;
            ctx.stroke();
        }
        ctx.restore();
    }
    

    有关于矩形的绘制和对应函数封装,在前面的Canvas绘制矩形一文中或多或少的介绍过,对于arcTo()的详细使用,可以阅读Canvas绘制圆和圆弧一文。

    圆、圆弧和扇形

    在Canvas中可以使用arc()arcTo()绘制圆弧、圆和扇形等基本形状。当绘制圆弧时,当startAngle角度值到endAngle角度值是0~360时,就可以绘制一个圆。所以我们在这里只需要封装两个函数:

    • drawArc():圆弧(或圆)函数
    • drawSector():扇形函数

    他们具有相同的参数,圆心(x,y)、半径radiusstartAngle起始弧度、endAngle结束弧度和anticlockwise旋转方向。由于startAngleendAngle只接受弧度单位值,所以在封装这两个函数之前,先封装一个角度deg和弧度rad之间的转换函数,方便后面函数的使用:

    // 将角度转换为弧度
    // @param {Number} deg - 角度值
    function getAngle(deg) {
        return Math.PI * deg / 180;
    }
    
    // 绘制圆弧或圆
    // 分类:填充圆弧和边框圆弧
    // @param {Number} x - 圆心x轴坐标
    // @param {Number} y - 圆心y轴坐标
    // @param {Number} radius - 圆弧的半径
    // @param {Number} startAngle - 开始的弧度(开始角度),只接受弧度单位,需要将deg先转换为rad: rad = Math.PI * deg / 180
    // @param {Number} endAngle - 结束的弧度(结束的角度)
    // @param {Boolean} anticlockwise - 旋转方向;true为逆时针,false为顺时针
    // @param {Boolean} isFill - 是否填充;true为填充,false为边框
    // @param {Boolean} isOnlyArc - 是否仅绘制弧边,如果使用closePath()终点和起点会连接到一起,否则不会。true时不连接,false连接
    // @param {String} color - 圆弧的颜色
    function drawArc(x, y, radius, startAngle, endAngle, anticlockwise, isOnlyArc, isFill, color) {
    
        if (isFill) {
            ctx.fillStyle = color;
            ctx.save();
            ctx.beginPath();
            ctx.arc(x, y, radius, getAngle(startAngle), getAngle(endAngle), anticlockwise);
            ctx.closePath();
            ctx.fill();
            ctx.restore();
        } else {
            ctx.strokeStyle = color;
            ctx.save();
            ctx.beginPath();
            ctx.arc(x, y, radius, getAngle(startAngle), getAngle(endAngle), anticlockwise);
    
            if (isOnlyArc) {
    
            } else {
                ctx.closePath();
            }
            ctx.stroke();
            ctx.restore();
        }
    } 
    
    // 绘制扇形
    // @param {Number} x - 圆心x轴坐标
    // @param {Number} y - 圆心y轴坐标
    // @param {Number} radius - 圆半径
    // @param {Number} startAngle - 开始弧度
    // @param {Number} endAngle - 结束弧度
    // @param {Number} anticlockwise - 旋转方向, true逆时针,false顺时针
    // @param {Boolean} isFill - true为填充,false为边框
    // @param {String} color - 扇形的颜色
    function drawSector(x, y, radius, startAngle, endAngle, anticlockwise, isFill, color) {
        ctx.save();
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.arc(x, y, radius, getAngle(startAngle), getAngle(endAngle), false);
        ctx.closePath();
    
        if (isFill) {
            ctx.fillStyle = color;
            ctx.fill();
        } else {
            ctx.strokeStyle = color;
            ctx.stroke();
        }
        ctx.restore();
    }
    

    绘制箭头

    在Canvas中没有直接的方法可以绘制箭,但@Patrick Horgan在《Drawing lines and arcs with arrow heads on HTML5 Canvas》一文中把绘制箭对的函数已经封装好了。我直接把代码放这里:

    // From: http://www.dbp-consulting.com/tutorials/canvas/CanvasArrow.html
    // Draw arrow head
    function drawHead (x0, y0, x1, y1, x2, y2, style, color, width) {
    
        if (typeof(x0) == 'string') {
            x0 = parseInt(x0);
        }
    
        if (typeof(y0) == 'string') {
            y0 = parseInt(y0);
        }
    
        if (typeof(x1) == 'string') {
            x1 = parseInt(x1);
        }
    
        if (typeof(y1) == 'string') {
            y1 = parseInt(y1);
        }
    
        if (typeof(x2) == 'string') {
            x2 = parseInt(x2);
        }
    
        if (typeof(y2) == 'string') {
            y2 = parseInt(y2);
        }
    
        var radius = 3,
        twoPI = 2 * Math.PI;
    
        ctx.save();
        ctx.beginPath();
        ctx.strokeStyle = color;
        ctx.fillStyle = color;
        ctx.lineWidth = width;
        ctx.moveTo(x0, y0);
        ctx.lineTo(x1, y1);
        ctx.lineTo(x2, y2);
    
        switch (style) {
            case 0:
                var backdist = Math.sqrt(((x2 - x0) * (x2 - x0)) + ((y2 - y0) * (y2 - y0)));
                ctx.arcTo(x1, y1, x0, y0, .55 * backdist);
                ctx.fill();
                break;
            case 1:
                ctx.beginPath();
                ctx.moveTo(x0, y0);
                ctx.lineTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.lineTo(x0, y0);
                ctx.fill();
                break;
            case 2:
                ctx.stroke();
                break;
            case 3:
                var cpx = (x0 + x1 + x2) / 3;
                var cpy = (y0 + y1 + y2) / 3;
                ctx.quadraticCurveTo(cpx, cpy, x0, y0);
                ctx.fill();
                break;
            case 4:
                var cp1x, cp1y, cp2x, cp2y, backdist;
                var shiftamt = 5;
                if (x2 == x0) {
                    backdist = y2 - y0;
                    cp1x = (x1 + x0) / 2;
                    cp2x = (x1 + x0) / 2;
                    cp1y = y1 + backdist / shiftamt;
                    cp2y = y1 - backdist / shiftamt;
                } else {
                    backdist = Math.sqrt(((x2 - x0) * (x2 - x0)) + ((y2 - y0) * (y2 - y0)));
                    var xback = (x0 + x2) / 2;
                    var yback = (y0 + y2) / 2;
                    var xmid = (xback + x1) / 2;
                    var ymid = (yback + y1) / 2;
                    var m = (y2 - y0) / (x2 - x0);
                    var dx = (backdist / (2 * Math.sqrt(m * m + 1))) / shiftamt;
                    var dy = m * dx;
                    cp1x = xmid - dx;
                    cp1y = ymid - dy;
                    cp2x = xmid + dx;
                    cp2y = ymid + dy;
                }
                ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x0, y0);
                ctx.fill();
                break;
            }
            ctx.restore();
        }
    
        // draw arrow
        function drawArrow(x1, y1, x2, y2, style, which, angle, d, color, width) {
    
            if (typeof(x1) == 'string') {
                x1 = parseInt(x1);
            }
    
            if (typeof(y1) == 'string') {
                y1 = parseInt(y1);
            }
    
            if (typeof(x2) == 'string') {
                x2 = parseInt(x2);
            }
    
            if (typeof(y2) == 'string') {
                y2 = parseInt(y2);
            }
    
            style = typeof(style) != 'undefined' ? style : 3;
            which = typeof(which) != 'undefined' ? which : 1;
            angle = typeof(angle) != 'undefined' ? angle : Math.PI / 9;
            d = typeof(d) != 'undefined' ? d : 10;
            color = typeof(color) != 'undefined' ? color : '#000';
            width = typeof(width) != 'undefined' ? width : 1;
            var toDrawHead = typeof(style) != 'function' ? drawHead : style;
            var dist = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
            var ratio = (dist - d / 3) / dist;
            var tox, toy, fromx, fromy;
    
            if (which & 1) {
                tox = Math.round(x1 + (x2 - x1) * ratio);
                toy = Math.round(y1 + (y2 - y1) * ratio);
            } else {
                tox = x2;
                toy = y2;
            }
    
            if (which & 2) {
                fromx = x1 + (x2 - x1) * (1 - ratio);
                fromy = y1 + (y2 - y1) * (1 - ratio);
            } else {
                fromx = x1;
                fromy = y1;
            }
    
            ctx.beginPath();
            ctx.strokeStyle = color;
            ctx.lineWidth = width;
            ctx.moveTo(fromx, fromy);
            ctx.lineTo(tox, toy);
            ctx.stroke();
    
            var lineangle = Math.atan2(y2 - y1, x2 - x1);
            var h = Math.abs(d / Math.cos(angle));
    
            if (which & 1) {
                var angle1 = lineangle + Math.PI + angle;
                var topx = x2 + Math.cos(angle1) * h;
                var topy = y2 + Math.sin(angle1) * h;
                var angle2 = lineangle + Math.PI - angle;
                var botx = x2 + Math.cos(angle2) * h;
                var boty = y2 + Math.sin(angle2) * h;
                toDrawHead(topx, topy, x2, y2, botx, boty, style, color, width);
            }
    
            if (which & 2) {
                var angle1 = lineangle + angle;
                var topx = x1 + Math.cos(angle1) * h;
                var topy = y1 + Math.sin(angle1) * h;
                var angle2 = lineangle - angle;
                var botx = x1 + Math.cos(angle2) * h;
                var boty = y1 + Math.sin(angle2) * h;
                toDrawHead(topx, topy, x1, y1, botx, boty, style, color, width);
            }
        }
    
        // draw arced arrow
        function drawArcedArrow(x, y, r, startangle, endangle, anticlockwise, style, which, angle, d, color, width) {
    
            style = typeof(style) != 'undefined' ? style : 3;
            which = typeof(which) != 'undefined' ? which : 1;
            angle = typeof(angle) != 'undefined' ? angle : Math.PI / 8;
            d = typeof (d) != 'undefined' ? d : 10;
            color = typeof(color) != 'undefined' ? color : '#000';
            width = typeof(width) != 'undefined' ? width : 1;
    
            ctx.save();
            ctx.beginPath();
            ctx.lineWidth = width;
            ctx.strokeStyle = color;
            ctx.arc(x, y, r, startangle, endangle, anticlockwise);
            ctx.stroke();
            var sx, sy, lineangle, destx, desty;
            ctx.strokeStyle = 'rgba(0,0,0,0)';
    
            if (which & 1) {
                sx = Math.cos(startangle) * r + x;
                sy = Math.sin(startangle) * r + y;
                lineangle = Math.atan2(x - sx, sy - y);
    
                if (anticlockwise) {
                    destx = sx + 10 * Math.cos(lineangle);
                    desty = sy + 10 * Math.sin(lineangle);
                } else {
                    destx = sx - 10 * Math.cos(lineangle);
                    desty = sy - 10 * Math.sin(lineangle);
                }
                drawArrow(sx, sy, destx, desty, style, 2, angle, d, color, width);
            }
    
            if (which & 2) {
                sx = Math.cos(endangle) * r + x;
                sy = Math.sin(endangle) * r + y;
                lineangle = Math.atan2(x - sx, sy - y);
    
                if (anticlockwise) {
                    destx = sx - 10 * Math.cos(lineangle);
                    desty = sy - 10 * Math.sin(lineangle);
                } else {
                    destx = sx + 10 * Math.cos(lineangle);
                    desty = sy + 10 * Math.sin(lineangle);
                }
    
                drawArrow(sx, sy, destx, desty, style, 2, angle, d, color, width);
            }
            ctx.restore();
        }
    

    是不是好复杂呀。如果上面代码看起来痛苦的话,可以阅读@Patrick Horgan写的《Drawing lines and arcs with arrow heads on HTML5 Canvas》文章或者阅读早前整理的Canvas绘制箭头一文。

    绘制正多边形

    绘制正多边形也相对于其他的绘图函数封装而言也较为复杂一点。我们将封装一个drawStarPolygons()函数,这个函数既能实现正多边形绘制,也能实现星形多边形的绘制。具体代码如下:

    // 绘制正多边形
    // @param {Number} xCenter 中心坐标X点
    // @param {Number} yCenter 中心坐标Y点
    // @param {Number} radius 外圆半径
    // @param {Number} sides 多边形边数
    // @param {Number} sideIndent (0 ~ 1)
    // @param {Number} alpha 角度 默认270度
    // @param {Boolean} isFill true填充,false边框
    // @param {String} color 正多边形颜色 
    function drawStarPolygons(xCenter, yCenter, radius, sides, sideIndent, alpha, isFill, color) {
    
        var sideIndentRadius = radius * (sideIndent || 0.38);
        var radAngle = alpha ? alpha * Math.PI / 180 : -Math.PI / 2;
        var radAlpha = Math.PI * 2 / sides / 2;
    
        ctx.save();
        ctx.beginPath();
    
        var xPos = xCenter + Math.cos(radAngle) * radius;
        var yPos = yCenter + Math.sin(radAngle) * radius;
    
        ctx.moveTo(xPos, yPos);
    
        for (var i = 1; i <= sides * 2; i++) {
            var rad = radAlpha * i + radAngle;
            var len = (i % 2) ? sideIndentRadius : radius;
            var xPos = xCenter + Math.cos(rad) * len;
            var yPos = yCenter + Math.sin(rad) * len;
    
            ctx.lineTo(xPos, yPos);
    
        }
    
        ctx.closePath();
    
        if (isFill) {
            ctx.fillStyle = color;
            ctx.fill();
        } else {
            ctx.strokeStyle = color;
            ctx.stroke();
        }
    }
    

    详细的可以阅读Canvas绘制正多边形一文。

    示例

    前面我们封装了一些我们常常需要使用的绘图函数。那我们拿一个示例来验证一下。比如说我们要绘制一个时钟:

    var canvas = document.getElementById('canvasOne');
    var ctx = canvas.getContext('2d');
    var w = canvas.width = window.innerWidth;
    var h = canvas.height = window.innerHeight;
    initDrawCanvas(canvas, ctx);
    
    // 绘制一个时钟
    
    
    var radius = 150;
    var handTruncation = canvas.width / 25;
    var hourHandTruncation = canvas.width / 10;
    
    // 绘制时钟刻度盘
    function drawClockFace() {
    
        // step1: 绘制时钟的外圆和圆心
        ctx.lineWidth = 4;
        ctx.translate(w / 2, h / 2);
    
        drawArc(0, 0, radius, 0, 360, true, true, false, '#000');
        drawArc(0, 0, 10, 0, 360, true, true, true, '#000');
    
        // step2: 绘制时钟刻度线
        for (var i = 0; i < 60; i++) {
            var rad = getAngle(i * 6);
            ctx.save();
            ctx.rotate(rad);
    
            if (i % 5 === 0) {
                drawSolidLine(radius - 15, -1, radius - 4, -1, 4, '#000');
            } else {
                drawSolidLine(radius - 8, -1, radius - 4, -1, 2, '#999');                   
            }
            ctx.restore();
        }
    
        // step3: 绘制时钟数字
        ctx.font = radius * 0.15 + "px arial";
        ctx.textBaseline = "middle";
        ctx.textAlign = "center";
    
        for (var i = 1; i < 13; i++) {
            var ang = getAngle(30 * i);
            ctx.fillText(i.toString(), radius * 0.80 * Math.sin(ang), -radius * 0.80 * Math.cos(ang));
        }
    }
    
    // 绘制时钟针
    function drawHand(angle, length, width, color) {
        var endX = Math.sin(angle) * length;
        var endY = -Math.cos(angle) * length;
        ctx.lineCap = 'round';
        drawSolidLine(0, 0, endX, endY, width, color);
    }
    
    function drawHands(radius) {
        var now = new Date();
        var hour = now.getHours();
        var minute = now.getMinutes();
        var second = now.getSeconds();
    
        hour = hour % 12;
        hour = getAngle(30) * hour + getAngle(30) * minute / 60 + getAngle(30) * second / 3600;
        minute = Math.PI / 30 * minute + second * Math.PI / 1800;
        second = Math.PI / 30 * second;
        drawHand(hour, radius * 0.4, radius * 0.07); // 时针
        drawHand(minute, radius * 0.6, radius * 0.05); // 分针
        drawHand(second, radius * 0.7, radius * 0.03, 'red'); // 秒针
    }
    
    function drawClock() {
        ctx.resetTransform();
        drawRect(0, 0, w, h, true);
        drawClockFace();
        drawHands(radius);
    }
    setInterval(drawClock, 1000);
    

    最终效果如下:

    总结

    这篇文章我们整理了Canvas中CanvasRenderingContext2D对象中自带绘制基本图形的方法、属性和样式。并且借助这些方法封装了一些绘制基本图形的函数,比如绘制线段、矩形、圆和正多边形的。最后绘制了一张图,把相关的知识汇总在一起。这篇文章也是介绍Canvas绘制基本图形的最后一篇文章了。



  • 相关阅读:
    background-position 使用方法具体介绍
    Android平台上直接物理内存读写漏洞的那些事
    自己编写高负荷測试的工具
    String,StringBuffer与StringBuilder的差别??
    shell之here文档
    心跳检测的思路及代码
    高可用架构篇--MyCat在MySQL主从复制基础上实现读写分离
    MySQL主从复制之Mycat简单配置和高可用
    Mycat 读写分离+分库分表
    MyCat:对MySQL数据库进行分库分表
  • 原文地址:https://www.cnblogs.com/qq984064199/p/9228358.html
Copyright © 2020-2023  润新知