canvas入门&绘制动态饼图

canvas是什么就不介绍了,我是怎么了解到canvas的我自己都不记得了,我只知道开始学习了解之后感觉就是欲罢不能啊,这东西很酷也很强大,而我喜欢的原因就是它能够进行画画!当然它主要的用途是进行数据可视化,echarts就是canvas写的。不多说了,这里分两部分,第一部分是简单的入门,第二部分是绘制一个动态饼图。

入门

使用canvas你就理解为画画就好了,画画需要画布、画笔(或者说是工具箱,工具箱中有画笔这些),有了这两样东西就可以画画了,画画需要确定开始位置、结束位置,开始位置和结束位置之间如何连线的问题,然后是关注画笔的笔头形状,画笔颜色,填充颜色。基本上使用canvas就是这样一个思路了

<style>
	canvas{
		border:1px solid #ccc;
	}
</style>
<!--
不要通过css进行画布大小的设置,canvas默认大小是300*150,如果通过css
设置大小,其实是设置画布内容区域的大小,这会导致内容被拉大或缩小
-->
<canvas width="600" height="400"></canvas>
//获取画板
var myCanvas=document.querySelector('canvas');
//获取上下文,(工具箱)
var ctx=myCanvas.getContext('2d');
//开启新路径,这样防止被其他未关闭的路径的影响
ctx.beginPath();
//移动到点(100,100)开始位置
ctx.moveTo(100,100);
//画到点(100,200)结束位置
ctx.lineTo(100,200);
//线条颜色
ctx.strokeStyle="red";
//线条宽度
ctx.lineWidth=10;
//描边,上面是进行画画的初始定义,stroke方法才是真正将线条画在画布上
ctx.stroke();

上面就是一个最基本的东西,了解一个最简单的线段应该如何画,剩下的画圆、矩形、曲线等,都可以通过工具箱ctx找得到对应的工具进行绘制,查看canvas文档就可以。
注意:这里有线模糊的问题,原因是在进行绘制的时候是从线的中间画的,而显示器处理不了0.5这种问题,导致线变大变模糊,详细的东西自己去查查就知道了

路径闭合的问题

起始点moveTo和移动点lineTo的结束点无法完全闭合,会产生缺角的问题
如果确定要闭合路径,可以使用canvas的自动闭合办法,在描边之前闭合

ctx.closePath();

填充区域和非零环绕

对于闭合路径之间区域的填充问题,主要与路径轨迹的方向相关,这里会用到非零环绕原则,我个人的简记是顺加逆减
通过一张图来进行非零环绕原则的理解,图是来自简书上的,地址
在这里插入图片描述
对于S1区域,从该区域内部向外拉一条线L1,方向随意,可以看到与L1相交的路径轨迹只有一条,方向是逆时针,减1,则相交轨迹总和为-1,不等于0,所以S1区域被填充。

对于S2区域,与L2相交的轨迹为两条,方向都为逆时针,相交轨迹总和为-2,不等于0,所以S2被填充

对于S3区域,与L3相加的轨迹为两条,一条为逆时针,减1,一条为顺时针,加1,则相交轨迹总和为0,所以S3区域不填充

绘制动态饼图

一般来说工作上使用canvas很多是用来做数据可视化(当然一般使用已有的库),也有用来做一些简单动画的,这里写的demo是参考了别人的写法进行的,原文见这里,我这里只写了如何进行动态效果的实现。后续有时间会做一个完整的。效果图如下:
在这里插入图片描述
源码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>动态饼图</title>
    <style>
        canvas {
            border: 1px solid #000;
        }
        .draw {
            display: inline-block;
            left: 50%;
            top: 50%;
            position: absolute;
            transform: translate(-50%, -50%);
        }
    </style>
</head>

<body>
    <div class="draw">
        <canvas width="600" height="400">你的浏览器不支持canvas</canvas>
    </div>
    <script>
        let ctx = document.querySelector("canvas").getContext("2d");
        data = [
            { range: "0 - 59", count: 9, color: "rgb(252, 54, 54)" },
            { range: "60 - 69", count: 15, color: "rgb(249, 252, 54)" },
            { range: "70 - 79", count: 23, color: "rgb(54, 252, 54)" },
            { range: "80 - 89", count: 17, color: "rgb(252, 54, 252)" },
            { range: "90 - 100", count: 12, color: "rgb(54, 90, 252)" }
        ]
        function DrawPipe(data) {
            this.data = data;
            this.ctx = document.querySelector("canvas").getContext("2d");
            this.width = this.ctx.canvas.width;
            this.height = this.ctx.canvas.height;
            // 圆心与半径
            this.x0 = this.width / 2;
            this.y0 = this.height / 2;
            this.r = 100;
            // 运动速率
            this.steps = 1;
            this.stepsCounts = 50;
            this.speed = 1.2;
            // 初始化相关需要的值
            this.counts = 0;
            this.dataTransformToAngle = [];
            this.startAngle = 0;
            this.endAngle = 0;
            const that = this;
            // 鼠标移入坐标
            this.mousePosition = {};
            this.mouseTimer = null;
            // 计算总和
            this.data.forEach(element => {
                that.counts += element.count;
            });
            // 计算角度
            this.data.forEach(item => {
                // 我这里直接进行角度的转换了,和参考文章不一样,要注意了
                // 一开始不注意困扰了我好久
                item.angle = item.count / that.counts * Math.PI * 2;
                that.dataTransformToAngle.push(item);
            });
        }
        DrawPipe.prototype.init = function () {
            this.dynamicPipe();
            this.addEvent();
        }
        // 开始画圆
        // 思路:将一个圆分成多个部分进行绘制。同时进行旋转使人眼看不到重新绘制的过程
        DrawPipe.prototype.dynamicPipe = function () {
            const that = this;
            // 这里用到save和restore方法,想想为什么
            this.ctx.save();
            // 这里是进行坐标系的偏移和设置旋转,如果要使用的话,圆心的位置要进行重新设置
            this.ctx.translate(this.x0, this.y0);
            this.ctx.rotate((Math.PI * 2 / this.stepsCounts) * this.steps / 2);

            this.dataTransformToAngle.forEach((item, i) => {
                // 这里增加的角度不能是固定乘以多少倍,不然得到的值都是固定的
                that.endAngle = that.endAngle + item.angle * that.steps / that.stepsCounts;
                that.ctx.beginPath();
                // 圆心的位置进行重新设置,半径看自己需要
                that.ctx.moveTo(that.x0 - 300, that.y0 - 200);
                that.ctx.arc(that.x0 - 300, that.y0 - 200, that.r * that.steps / that.stepsCounts, that.startAngle, that.endAngle);

                if (that.ctx.isPointInPath(that.mousePosition.x, that.mousePosition.y)) {
                    that.ctx.globalAlpha = 0.5;
                }

                that.ctx.closePath();
                that.ctx.fillStyle = item.color;
                that.ctx.fill();
                that.ctx.globalAlpha = 1;
                that.startAngle = that.endAngle;

                // 画完一个周期把起始位置恢复
                if (i == that.dataTransformToAngle.length - 1) {
                    that.startAngle = 0;
                    that.endAngle = 0;
                }

            });

            this.ctx.restore();

            if (this.steps < this.stepsCounts) {
                // 这个steps作用很重要,决定了动画的状态
                this.steps++;
                setTimeout(() => {
                    that.ctx.clearRect(0, 0, that.width, that.height);
                    that.dynamicPipe();
                }, that.speed *= 1.085);
            }
        }
        DrawPipe.prototype.addEvent = function () {
            this.ctx.canvas.addEventListener("mousemove", function (e) {
                // 这里就不做兼容了
                this.mousePosition.x = e.offsetX;
                this.mousePosition.y = e.offsetY;
                clearTimeout(this.mouseTimer);
                this.mouseTimer = setTimeout(() => {
                    this.ctx.clearRect(0, 0, this.width, this.height);
                    this.dynamicPipe();
                }, 0);
            }.bind(this))
        }

        let test = new DrawPipe(data);
        test.init();
    </script>
</body>

</html>
发布了28 篇原创文章 · 获赞 1 · 访问量 8730

猜你喜欢

转载自blog.csdn.net/moqiuqin/article/details/95475793