利用canvasApi来绘制一个水球图的,除了继续熟悉canvas的基本使用和三次贝塞尔曲线,同时为了引入新的canvas裁剪功能和抗锯齿的技巧。效果图如下:gitee源码、github源码
分析准备
分析整个水球图,波形的绘制就是整个绘制过程的最大的难点。绘制波形,第一个难点就是波纹的形状,其实可以选择两种方案绘制,第一种,采取圆弧拼接的形式;第二种就是采用三次贝塞尔曲线来绘制曲线。为了再次熟悉三次贝塞尔曲线的绘制过程,所以采用了方式二。波形的绘制难点二就是如何使得波形绘制在圆内,此处便要引入clip、save、restore三个函数的使用了,其详细情况可见MDN文档。
绘制水球图的第二个难点,就是如何绘制水漫文字的效果。水漫文字的效果我们观察发现,未被水漫部分则为最上层颜色,否则为白色。所以,我们就可以考虑先绘制一个带底色的文字,之后再利用裁剪效果,来绘制被水漫的白色部分。
- 先绘制一个带底色的文字
- clip裁剪后在相同位置绘制一个相同的白色文字
我们总共绘制了三层水波纹,在每层绘制水波纹的时候都需要绘制白色部分,否则后面的谁的波纹会将文字盖住。但是带底色的文字只需要在最下层波纹绘制的时候绘制即可。
最后,在水球图绘制完成后,我们会发现圆圈外侧并不光滑,成锯齿状,此时,我们则需要使用canvas的抗锯齿技巧,其实抗锯齿很简单,就像大家在在凑很近看墙面时候,你可能发现墙面是坑坑洼洼的,但是站的远,就可能觉得墙面很平整。因为站的远就看不清细节,我们脑子机自动的认为墙面平整了,抗锯齿也可以利用一样的原理,只要看不清真实边缘,那么边缘则看起来就比较平滑了。所以要用到阴影来模糊掉边缘即可,需要使用到shadowColor shadowBlur shadowOffsetX shadowOffsetY这几个属性。
代码示例
完整源码见文章开始贴的源码仓库
function WaterCahrt(id, data) { let canvas = document.getElementById(id) let ctx = canvas.getContext('2d') let canvasWidth = canvas.width > canvas.height ? canvas.height : canvas.width let r = canvasWidth * 0.8 / 2 let offset = [0, 20, 40] this.canvas = canvas this.ctx = ctx this.data = data this.r = r // 圆半径 this.center = canvasWidth / 2 //圆心x(y) this.intervalId = null Object.defineProperty(this, 'offset', { // 定义水波图的偏移量,当偏移量发生变化时自动触发更新enumerable: false,configurable: false,get() { return offset },set(val) { offset = val this.refresh() } }) }复制代码
此处offset设置成了个数组,是为了使三个水波的移动速度具有一定差距。
/** * 绘制水波图 * @param {number} basex 波纹的起始x值 * @param {number} basey 波纹的起始点y值 * @param {strign} color 波纹颜色 * @param {boolean} flag 是否绘制与波纹相同颜色的文字,理论上只需要最下层的波纹才需要绘制 */WaterCahrt.prototype.drawWater = function(basex, basey, color, flag) { let bezier = this.getBezierPoints(basex, basey) // 生成三次贝塞尔曲线 this.ctx.save() // 存储画笔状态 // 绘制波形 this.ctx.beginPath() this.ctx.moveTo(this.center - this.r, this.center + this.r) this.ctx.lineTo(bezier.x, bezier.y) bezier.points.forEach((item) => {this.ctx.bezierCurveTo(item.cp1x, item.cp1y, item.cp2x, item.cp2y, item.x, item.y) }) this.ctx.lineTo(this.center + this.r, this.center + this.r) this.ctx.closePath() this.ctx.fillStyle = color this.ctx.shadowColor = color //抗锯齿 this.ctx.shadowBlur = 2 this.ctx.fill() this.ctx.shadowBlur = 0 //绘制带底色文字 this.ctx.font = `normal ${this.r * 0.2}px blod` this.ctx.textAlign = "center" if(flag)this.ctx.fillText(`${(this.data * 100).toFixed(1)}%`, this.center, this.center) // 绘制文字白色部分 this.ctx.clip() // 按照所绘路径裁剪 this.ctx.fillStyle = '#ffffff' this.ctx.fillText(`${(this.data * 100).toFixed(1)}%`, this.center, this.center) this.ctx.restore() // 恢复画笔状态,即到未裁剪之前状态 return this}复制代码
三次贝塞尔曲线的点的生成不在赘述,详细生成原理可参考文章《贝塞尔曲线控制点确定的方法》。
总结
此次水球图的绘制,主要是为了对clip、save、restore相关的方法的尝试,同时还引入了抗锯齿的一个小技巧。整体绘制上计算量相对并不大,且无交互效果,只是存在一个动画效果,动画效果的实现则是使用setInterval间隔绘制动画帧即可。整体上来说并没有太大的难度。