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>