在日常生产生活中,我们经常会阅读或使用各种类型的图表。圆环(圆弧)是一种比较常见的类型,用来直观地表示某项数据指标占整体的比例。本文重点介绍HTMLCanvas的实现(当然SVG党了解原理后可以自行实现),逐层介绍圆环图开发的一些主要思想和原理。图1展示了一些我们平时比较常见的环(弧)效应。虽然图形的主要成分是弧线,但不同的效果对信息传递的作用略有不同。例如,一个闭合的环可以代表一个过程“进展”的概念。开口环一般用于显示状态量(标量),一般也称为“仪表盘”。可以用不同的色调来标识状态量的状态区间(例如:低风险-中风险-高风险区间的标志可以用三种颜色组成的过渡效果来表示)为了更方便和完美的解决了我们在业务开发中的特定需求,我们可以对这些样式和样式进行一定的分析、抽象,总结出一个通用组件需要具备的能力,比如:可以配置颜色和渐变(支持传入单个颜色值或颜色顺序)弧宽可调(可配置内外半径)弧角可调(可配置起始角、截止角)弧端点形状可选(可切换平角/半圆)形状复制效果可调(字体大小、字体、颜色等)接下来,我们着手实现这样一个功能全面、业务通用性强的戒指组件。1.戒指的形状第一步绘制戒指的形状,需要画出环形图的构成元素,也就是一段弧线。对于下图的两种倒角效果(黄色部分圆弧两端的样式),可以是直角也可以是半圆。因此,我们需要实现一个通用的方法来绘制圆弧,并提供两种倒角样式给用户。1.1绘制前景圆弧绘制圆弧的思路如上图所示,大致可以依次分为几个步骤:(1)绘制圆弧起点的半圆轮廓(2)绘制外圆圆弧的边缘轮廓(3)绘制圆弧末端的半圆轮廓(4)绘制圆弧的内边缘轮廓(5)关闭轮廓并填充颜色注意:由于在canvas中绘制圆弧的方法默认是顺时针方向,我们的绘制步骤也是顺时针方向以下是一些姿势要领:1.1.1端点坐标的计算在绘制端点半圆之前,我们需要端点的位置坐标,以实际端点为例,根据到圆环的半径(内外径的平均值,即圆弧中心线的半径)如何从与起点终点的夹角计算出圆上一点的坐标://计算圆弧上一点的坐标//originX,originY-圆心坐标//radius-圆的半径,等于圆内外径的平均值,即圆弧中心线的半径//alpha-radianfunctioncalcPosition(originX,originY,radius,alpha){return[radius*Math.cos(alpha)+originX,radius*Math.sin(alpha)+originY,];}1.1.2端点处半圆的起始角和终止角要在画布中绘制圆弧,需要知道它的起始角和终止角。由于canvas绘图默认的方向是屏幕的顺时针方向(屏幕Z轴的左手螺旋方向),所以从上面的示意图可以看出:起始端半圆的范围-[radianStart-Math.PI,radianStart]端点半圆的范围end-[radianEnd,radianEnd+Math.PI]1.1.3画半圆有了端点坐标和起止角,就可以画半圆了attheendpoint://以起始端半圆的倒角为例myCanvas.context.arc(x,y,(radiusOutter-radiusInner)/2,//小圆的半径等于一半圆的线宽radianStart-Math.PI,radianStart);直角倒角样式的绘制步骤与半圆倒角圆弧的绘制步骤基本相同,主要区别在于圆弧的两个端点不是画一个小半圆,而是画一条直线线代替。背景弧线的绘制也与前景弧线的绘制方法一致。2.Canvas实现锥形渐变。以上几步就可以画出圆弧的轮廓了。为了达到如图1所示的视觉效果,我们需要用之前绘制的轮廓填充图像。沿圆周渐变,因为它的图像看起来像一个圆锥形的俯视效果,俗称锥形渐变:众所周知,CSS中有一个属性叫conic-gradient,直接支持圆锥渐变,而HTML的原生APICanvas还没有类似的能力。那么,我们如何在画布上绘制这样的图像呢?下面说说大概的原理:(1)对用户传入的颜色进行插值,得到一个颜色序列。这里,我们直接使用canvas原生的createLinearGradient方法在离屏画布中绘制一个1px的线性渐变效果。图片宽度就是我们要插值的量,梯度插值的结果就是画布上对应像素位置的颜色值。颜色插值(渐变选色)代码实现如下://用于实现颜色插值的工具类exportdefaultclassColorInterpolate{//参数01:stops-为待插值的颜色序列,数据格式如下如下:[[0,'red'],[0.5,'green'],[1.0,'yellow']]//参数02:segment——插值段数,即颜色值的个数插值结果的constructor(stops=[],segment=100){//构建离屏画布constcanvas=document.createElement('canvas');canvas.width=段;canvas.height=1;this.ctx=canvas.getContext('2d');//绘制线性渐变constgradient=this.ctx.createLinearGradient(0,0,segment,0);for(let[offset,color]ofstops){gradient.addColorStop(offset,color);}this.ctx.fillStyle=渐变;this.ctx.fillRect(0,0,段,1);}//根据位置偏移获取插值颜色值getColor(offset){constimgData=this.ctx.getImageData(offset,0,1,1);返回`rgba(${imgData.data.slice(0,3).join(',')},${imgData.data[3]/255})`;}}(2)如下图所示,我们可以将渐变图像想象成由足够小的“扇形”拼接而成的马赛克,以填充单个颜色值。按照这个思路,我们只需要遍历上面颜色插值得到的每一种颜色,然后把小扇子一个一个画出来,就可以得到一个锥形的渐变图。为此,我们封装了一个名为createConicalGradient的方法,其使用习惯类似于canvas原有的createLinearGradient和createRadialGradient方法。具体代码见我的Github(如果觉得有用,可以star)。3.过渡动画3.1圆弧过渡当值发生变化时,我们的图表需要一个能够跟随数据变化的过渡动画效果。对于canvas来说,就是清除旧图像,然后绘制一帧新图像。下面是方法封装的一些技巧://注意:伪代码,真实场景建议OOP封装为工具类let_animTick=null;let_animFrames=null;let_frameData=null;let_animDiff=null;//动画方法函数_animate(duration){if(_animTick===null){//根据动画时长计算整个动画需要的总帧数(以60fps计算)_animFrames=Math.round((duration/1e3)*60);//两个相邻动画帧的相对数据变化_animDiff=_calcAnimDiff(_animFrames);//动画帧数标识_animTick=0;}//当前帧数据值_frameData=_caclCurentData(_animDiff,_animTick);_renderFrame(_frameData);if(_animTick!==null&&_animTick<_animFrames){//继续动画window.requestAnimationFrame(()=>{_animate();_animTick+=1;});}else{//动画结束_renderFrame(_frameData);_animTick=空;}}//绘制当前帧函数_renderFrame(data){//...}//计算动画函数相邻帧的数据差_calcAnimDiff(){//...}//根据两帧当前帧数据差异计算函数_caclCurentData(){//...}3.2两种动画模式在过渡动画执行过程中,需要考虑两种不同的模式:一种是渐变图像不变化,只圆弧轮廓从旧状态变为新状态;一是渐变图像的角度范围随着轮廓的大小而变化。这两种模式其实都有一定的含义:前者可以用不同的颜色来表示数值的不同状态;后者只是将渐变色作为一种装饰效果。4.介绍了结果论证的主要原则。最后展示一下我们封装的图表组件的效果(右边的GUI部分是我们自研设计引擎的编辑效果):5.后面写的这篇文章是为可视化图表开发的一个小案例也是[图表开发小案例]系列第一篇文章。篇幅比较短,还有很多细节没有讲到。欢迎有兴趣的童鞋交流讨论,请多多关注我们的后续推文~(^_^)Y
