H5 Canvas绘制三角函数图像

三角函数sin、cos、tan、sinh、cosh、tanh

js 的Math库,自带了许多常见函数方法,其中关于 三角函数的求解还是很齐全的,于是想到,利用canvas 绘制一个比较准确的 sin、cos、tan、sinh、cosh、tanh图像。

canvas绘制坐标轴

坐标轴由两条直线构成,直接将坐标定在中心点就好,这样容易绘制。同时为坐标的箭头定义边长。


//xy轴
        function drawXY() {

            var ox = Math.floor(width / 2) + fix1px;//1px显示变粗修复
            var oy = Math.floor(height / 2) + fix1px;//1px显示变粗修复

            //x轴
            cxt.moveTo(0, oy);
            cxt.lineTo(width, oy);

            //y轴
            cxt.moveTo(ox, height);
            cxt.lineTo(ox, 0);
            // cxt.stroke();

            //x箭头
            // cxt.beginPath();
            cxt.moveTo(width - arrowLin -cxt.lineWidth/2, oy - arrowDui);
            cxt.lineTo(width, oy);
            cxt.lineTo(width - arrowLin -cxt.lineWidth/2, oy + arrowDui);
            // cxt.closePath();
            // cxt.stroke();

            //y箭头
            // cxt.beginPath();
            cxt.moveTo(ox - arrowDui, arrowLin+cxt.lineWidth/2);
            cxt.lineTo(ox, 0);
            cxt.lineTo(ox + arrowDui, arrowLin+cxt.lineWidth/2);
            // cxt.closePath();

            cxt.stroke();

        }

绘制坐标轴上的尺度和文字

思路:将坐标轴线的长度平均分割,分别定义好x轴和y轴每个尺度的对应值。
这其中,sin,cos和tan的图一般都是以π作为单位,且π/2也做标注,故画需要对每个单元格进行更细小的划分,按更小的单位进行循环绘制。类似直尺上的毫米,一毫米一毫米绘制,每十毫米满一个单元时,其标尺标记更长一些,以示区分。

为灵活展示每个单元的更细小划分情况,应允许传入一个单元的拆分数,用来展示更小单位下的尺度。如一厘米=10毫米,即拆分为10份。

对sin、cos、tan 来说,需要展示标注文字和标尺:1π/2,3π/2,则每个单元应拆分为2份。
但对sinh、cosh、tanh 来说,x、y轴上都不需要展示1/2,1/3的标注文字,也不需要展示特别细小的尺寸。份数可设置为1。

其实,这只是因为过小的尺寸不好标注文字。限制每多少个小单位标注文字即可。

//单元格
        function drawUnitXY() {
            //x轴分割
            var x = 0;
            //整除
            // var xExact = deConfig.cellXs % 2 === 0;

            var xText = '';
            cxt.beginPath();

            //x轴
            var xCount = Math.floor((deConfig.cellXs * deConfig.cellXMins) / 2);
            // xExact ? (xCount = xCount - 1) : xCount;
            cxt.font = deConfig.fontSize + 'px sinsum';


            // 起始和结束不绘制
            for (var i = 1; i <= xCount; i++) {
                //负数轴
                x = Math.floor(width / 2 - i * cellXL / deConfig.cellXMins);

                if (x > 0) {
                    x = x + fix1px;
                    //当i%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
                    if (i % deConfig.cellXMins === 0) {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH);
                    } else {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
                    }
                    cxt.lineTo(x, height / 2);

                    xText = -i / deConfig.cellXMins + "" + deConfig.cellXUnit;

                    if (i % (deConfig.spaceXMText + 1) === 0) {
                        cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
                    }

                }

                //正数轴
                x = Math.floor(width / 2 + i * cellXL / deConfig.cellXMins);
                if (x < width) {
                    x = x + fix1px;//1px显示粗细修复
                    if (i % deConfig.cellXMins === 0) {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH);
                    } else {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
                    }
                    cxt.lineTo(x, height / 2 - 0.5);

                    xText = i / deConfig.cellXMins + "" + deConfig.cellXUnit;
                    if (i % (deConfig.spaceXMText + 1) === 0) {
                        cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
                    }
                }
            }


            //y轴分割
            var y = 0;
            // var yExact = deConfig.cellYs % 2 === 0;
            var yCount = Math.floor(deConfig.cellYs * deConfig.cellYMins / 2);
            // yExact ? (yCount = yCount - 1) : yCount;
            //整除

            // 起始和结束不绘制
            for (var j = 1; j <= yCount; j++) {

                //负轴
                y = Math.floor(height / 2 + j * cellYL / deConfig.cellYMins);
                console.log(deConfig.fn, y, yCount, '下');
                if (y < height) {
                    y = y + fix1px;
                    //当j%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
                    if (j % deConfig.cellYMins === 0) {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
                    } else {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
                    }
                    cxt.lineTo(width / 2, y)
                    if (j % (deConfig.spaceYMText + 1) === 0) {
                        cxt.fillText(-j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
                    }
                }

                //正轴
                y = Math.floor(height / 2 - j * cellYL / deConfig.cellYMins);
                console.log(deConfig.fn, y, yCount, '上');
                if (y > 0) {
                    y = y + fix1px;
                    if (j % deConfig.cellYMins === 0) {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
                    } else {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
                    }
                    cxt.lineTo(width / 2, y)
                    if (j % (deConfig.spaceYMText + 1) === 0) {
                        cxt.fillText(j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
                    }
                }
            }
            cxt.closePath();
            cxt.stroke();
        }

此处采取坐标原点向正负方向绘制,这样绘制逻辑清晰一些,但代码量会增加。另一种方式是从左往右,或从右往左绘制,这样需要计算起始绘制位置等值。

绘制函数变化曲线

只需要将画布x轴上每一个像素点代表的x的值,带入函数,求出函数y的值,并转化为画布坐标轴的对应的值。由此求出函数变化曲线在画布上的每一个点坐标,连接起来即可。因此,画布有多宽,就循环多少次,求坐标点,以使曲线平滑。

//函数曲线
        function drawFnLine() {

            var startXV = -width / 2 * oneXV;//最左方x的值
            var startYV = height / 2 * oneYV;//最上方y的值

            var yv = 0;
            //为每个x像素点计算y值,并连接
            var y = 0;
            cxt.strokeStyle = deConfig.lineColor;
            cxt.beginPath();
            for (var x = 0; x <= width; x++) {

                eval("yv = Math." + deConfig.fn + "(startXV + oneXV * x);")

                y = (startYV - yv) / oneYV;
                if (Math.abs(x) > width || Math.abs(y) > height || x <= 0 || x >= width) {
                    cxt.moveTo(x + fix1px, y);
                    continue;
                }
                cxt.lineTo(x + fix1px, y);
            }
            cxt.closePath();
            cxt.stroke();
        }

绘制结果和注意事项

h5 canvas绘制一像素曲线时,可能导致2px曲线的结果。

1px变2px的原因

这是由于绘制时,将画笔中线对齐坐标,必然有一半线宽在坐标之外,这一半的线宽,如果不是整数,会被自动加满为整数。(因为1像素就是最小单位了,0.x的像素不能显示)

  • 当线宽为奇数,绘制坐标是整数时,那么无论如何,线条的矩形区域都坐标都多出0.5像素,于是补满,整体多出了1像素。
  • 当线宽为偶数,绘制坐标是整数时,那么无论如何,画笔中线总是对齐坐标的,线条宽度不会产生偏差。
  • 当线宽有小数,绘制坐标是整数时,那么无论如何,线条的矩形区域都坐标都存在小数,于是补满。整体也会多出1像素。

如:
当画笔线宽为1px,那么绘制直线L:(0,0),(100,0)时,画笔对齐 y 坐标 = 0。由于线宽的存在,实际绘制线条有一半线宽在y的负半轴。导致不可见。
那么线条矩形区域为:

扫描二维码关注公众号,回复: 2813538 查看本文章

Rect(left:0,top:-0.5,right:100;bottom:0.5)

0.5像素不可见,加满1像素,实际线条区域变为:

Rect(left:0,top:-1,right:100;bottom:1)

就变2px那么大了。

解决这个问题,我们只需要将线条的y坐标偏移0.5就行:
L:(0,0.5),(100,0.5)
当画笔中线对齐y坐标=0.5,时,线条矩形区域没有多余小数:

Rect(left:0,top:0,right:100,1)

这样,自然就是真的1px线条了。

综上,线宽为偶数不会产生误差,而线宽为小数,本来就无法准确绘制,不用处理了。

所以绘制时,我们只需要修正线宽为奇数的情况就行。甚至,只修复线宽1px的情况就可以,因为当线宽很大时,肉眼感知不到啊。

var fix1px = (cxt.lineWidth % 2 === 0) ?0: 0.5;

纵向线条,x坐标偏移:
x = x+fix1px ;

横向线条,y坐标偏移:
y = y+fix1px ;

1px线条变2px线条图示:

这里写图片描述

`

结果预览

如图:
这里写图片描述

完整代码:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <!--<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;">-->
    <title>三角函数图</title>
</head>
<body>


<canvas id="sinCanvas" height="300px" width="300px"></canvas>
<canvas id="cosCanvas" height="300px" width="300px"></canvas>
<canvas id="tanCanvas" height="300px" width="300px"></canvas>
<p></p>
<canvas id="sinhCanvas" height="300px" width="300px"></canvas>
<canvas id="coshCanvas" height="300px" width="300px"></canvas>
<canvas id="tanhCanvas" height="300px" width="300px"></canvas>

<script>

    //线宽为奇数不能被整除,导致绘制坐标为整数时,上下偏移出0.5像素。又由于0.5像素不能被显示,
    // 自动补足为1像素。于是整体像素+1。
    // 具体如
    //当绘制直线L:0,0),(100,0)时,画笔中线y坐标=0。由于线宽的存在,
    //实际绘制线条有一半线宽在y的负半轴。导致不可见。
    //如若绘制时,画笔中线坐标y为1,线宽也为1,那么线条区域Rect(left:0,top:-0.5,right:100;bottom:0.5)
    //由于0.5像素不足以被显示,实际渲染时,上下各加0.5,于是线条变成2px:
    // Rect(left:0,top:-1,right:100;bottom:1)
    //当线宽为2时,则不存在这个现象。
    //解决办法:
    // var fix1px = (cxt.lineWidth % 2 === 0) ?0: 0.5;
    //线宽不为整数,不用管。
    //纵向线条,x坐标偏移
    // x = x+fix1px ;
    //横向线条,y坐标偏移
    // y = y+ fix1px ;

    var angularHelper = function (canvas, config) {
        config = config ? config : {};
        var deConfig = {
            fn: config.fn ? config.fn : 'tan',
            fontSize: 12,
            // desColor:config.desColor?config.desColor:"",
            lineColor: config.lineColor ? config.lineColor : "#7389B9",//曲线颜色
            arrowL: config.arrowL ? config.arrowL : 4,//箭头线长
            arrowArg: Math.PI / 3,//箭头夹角
            cellXs: config.cellXs ? config.cellXs : 8,//x轴分割数
            cellYs: config.cellYs ? config.cellYs : 6,//y轴分割数
            cellLH: config.cellLH ? config.cellLH : 6,//分割线高度
            cellXV: config.cellXV ? config.cellXV : Math.PI,//每个x单元的值(弧度)
            cellYV: config.cellYV ? config.cellYV : 1,//每个y单元的值
            cellXUnit: (config.cellXUnit || config.cellXUnit === '') ? config.cellXUnit : 'π',//单位
            cellYUnit: (config.cellYUnit || config.cellYUnit === '') ? config.cellYUnit : '',//单位
            cellXMins: config.cellXMins ? config.cellXMins : 1,//x轴一单位尺度拆分为更小份数
            cellYMins: config.cellYMins ? config.cellYMins : 1,//y轴一单位尺度拆分为更小份数,
            spaceXMText: config.spaceXMText ? config.spaceXMText : 0,//x轴每间隔多少个更小单位,标注文字
            spaceYMText: config.spaceYMText ? config.spaceYMText : 0,//y轴每间隔多少个更小单位,标注文字


        }
        var cxt = canvas.getContext('2d');
        cxt.lineWidth = 1;
        var width = canvas.width;
        var height = canvas.height;
        var arrowL = deConfig.arrowL;
        //箭头三角形中线长
        var arrowLin = arrowL * Math.cos(deConfig.arrowArg / 2);
        //箭头三角形底边/2
        var arrowDui = arrowL * Math.sin(deConfig.arrowArg / 2);

        //每份x对应的像素长度
        var cellXL = width / deConfig.cellXs;
        var cellYL = height / deConfig.cellYs;

        //y=sin(x)
        var oneXV = deConfig.cellXV / cellXL;//一个x像素点对应的x值
        var oneYV = deConfig.cellYV / cellYL;//一个x像素点对应的y值

        var fix1px = (cxt.lineWidth % 2 === 0) ? 0 : 0.5;

        function draw() {
            drawDes();
            drawXY();
            drawUnitXY();
            drawFnLine();

        }

//描述
        function drawDes() {
            cxt.font = deConfig.fontSize + 'px sinsum';
            var fw = cxt.measureText('x').width;

            cxt.fillText("y = " + deConfig.fn + '(x)  x∈R', deConfig.fontSize, deConfig.fontSize);
            cxt.fillText('x', width - fw, height / 2 + deConfig.fontSize)
            cxt.fillText('y', width / 2 + fw, fw)
            // cxt.stroke();
        }

//xy轴
        function drawXY() {

            var ox = Math.floor(width / 2) + fix1px;//1px显示变粗修复
            var oy = Math.floor(height / 2) + fix1px;//1px显示变粗修复

            //x轴
            cxt.moveTo(0, oy);
            cxt.lineTo(width, oy);

            //y轴
            cxt.moveTo(ox, height);
            cxt.lineTo(ox, 0);
            // cxt.stroke();

            //x箭头
            // cxt.beginPath();
            cxt.moveTo(width - arrowLin -cxt.lineWidth/2, oy - arrowDui);
            cxt.lineTo(width, oy);
            cxt.lineTo(width - arrowLin -cxt.lineWidth/2, oy + arrowDui);
            // cxt.closePath();
            // cxt.stroke();

            //y箭头
            // cxt.beginPath();
            cxt.moveTo(ox - arrowDui, arrowLin+cxt.lineWidth/2);
            cxt.lineTo(ox, 0);
            cxt.lineTo(ox + arrowDui, arrowLin+cxt.lineWidth/2);
            // cxt.closePath();

            cxt.stroke();

        }

//单元格
        function drawUnitXY() {
            //x轴分割
            var x = 0;
            //整除
            // var xExact = deConfig.cellXs % 2 === 0;

            var xText = '';
            cxt.beginPath();

            //x轴
            var xCount = Math.floor((deConfig.cellXs * deConfig.cellXMins) / 2);
            // xExact ? (xCount = xCount - 1) : xCount;
            cxt.font = deConfig.fontSize + 'px sinsum';


            // 起始和结束不绘制
            for (var i = 1; i <= xCount; i++) {
                //负数轴
                x = Math.floor(width / 2 - i * cellXL / deConfig.cellXMins);

                if (x > 0) {
                    x = x + fix1px;
                    //当i%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
                    if (i % deConfig.cellXMins === 0) {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH);
                    } else {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
                    }
                    cxt.lineTo(x, height / 2);

                    xText = -i / deConfig.cellXMins + "" + deConfig.cellXUnit;

                    if (i % (deConfig.spaceXMText + 1) === 0) {
                        cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
                    }

                }

                //正数轴
                x = Math.floor(width / 2 + i * cellXL / deConfig.cellXMins);
                if (x < width) {
                    x = x + fix1px;//1px显示粗细修复
                    if (i % deConfig.cellXMins === 0) {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH);
                    } else {
                        cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
                    }
                    cxt.lineTo(x, height / 2 - 0.5);

                    xText = i / deConfig.cellXMins + "" + deConfig.cellXUnit;
                    if (i % (deConfig.spaceXMText + 1) === 0) {
                        cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
                    }
                }
            }


            //y轴分割
            var y = 0;
            // var yExact = deConfig.cellYs % 2 === 0;
            var yCount = Math.floor(deConfig.cellYs * deConfig.cellYMins / 2);
            // yExact ? (yCount = yCount - 1) : yCount;
            //整除

            // 起始和结束不绘制
            for (var j = 1; j <= yCount; j++) {

                //负轴
                y = Math.floor(height / 2 + j * cellYL / deConfig.cellYMins);
                console.log(deConfig.fn, y, yCount, '下');
                if (y < height) {
                    y = y + fix1px;
                    //当j%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
                    if (j % deConfig.cellYMins === 0) {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
                    } else {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
                    }
                    cxt.lineTo(width / 2, y)
                    if (j % (deConfig.spaceYMText + 1) === 0) {
                        cxt.fillText(-j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
                    }
                }

                //正轴
                y = Math.floor(height / 2 - j * cellYL / deConfig.cellYMins);
                console.log(deConfig.fn, y, yCount, '上');
                if (y > 0) {
                    y = y + fix1px;
                    if (j % deConfig.cellYMins === 0) {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
                    } else {
                        cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
                    }
                    cxt.lineTo(width / 2, y)
                    if (j % (deConfig.spaceYMText + 1) === 0) {
                        cxt.fillText(j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
                    }
                }
            }
            // cxt.closePath();
            cxt.stroke();
        }

//函数曲线
        function drawFnLine() {

            var startXV = -width / 2 * oneXV;//最左方x的值
            var startYV = height / 2 * oneYV;//最上方y的值

            var yv = 0;
            //为每个x像素点计算y值,并连接
            var y = 0;
            cxt.strokeStyle = deConfig.lineColor;
            cxt.beginPath();
            for (var x = 0; x <= width; x++) {

                eval("yv = Math." + deConfig.fn + "(startXV + oneXV * x);")

                y = (startYV - yv) / oneYV;
                if (Math.abs(x) > width || Math.abs(y) > height || x <= 0 || x >= width) {
                    cxt.moveTo(x + fix1px, y);
                    continue;
                }
                cxt.lineTo(x + fix1px, y);
            }
            // cxt.closePath();
            cxt.stroke();
        }

        return {draw: draw};
    }


    var sinCanvas = document.getElementById('sinCanvas');
    var cosCanvas = document.getElementById('cosCanvas');
    var tanCanvas = document.getElementById('tanCanvas');
    var sinhCanvas = document.getElementById('sinhCanvas');
    var coshCanvas = document.getElementById('coshCanvas');
    var tanhCanvas = document.getElementById('tanhCanvas');

    angularHelper(sinCanvas, {fn: 'sin', cellXV: Math.PI, cellXs: 5, cellYs: 7, cellXMins: 2, cellYMins: 2}).draw();
    angularHelper(cosCanvas, {fn: 'cos', cellXV: Math.PI, cellXs: 4, cellYs: 4, cellXMins: 2}).draw();
    angularHelper(tanCanvas, {fn: 'tan', cellXV: Math.PI, cellXs: 5, cellYs: 4, cellXMins: 2, spaceXMText: 1}).draw();

    angularHelper(sinhCanvas, {fn: 'sinh', cellXV: 1, cellXs: 8, cellXUnit: '', cellXMins: 10, spaceXMText: 4}).draw();
    angularHelper(coshCanvas, {fn: 'cosh', cellXV: 1, cellXs: 7, cellXUnit: ''}).draw();
    angularHelper(tanhCanvas, {fn: 'tanh', cellXV: 1, cellXs: 7, cellXUnit: ''}).draw();

</script>
</body>
</html>

猜你喜欢

转载自blog.csdn.net/Mingyueyixi/article/details/80964430