canvas之旅系列----(五)

1,153 阅读4分钟

canvas绘制水球图

利用canvasApi来绘制一个水球图的,除了继续熟悉canvas的基本使用和三次贝塞尔曲线,同时为了引入新的canvas裁剪功能和抗锯齿的技巧。效果图如下:gitee源码github源码

水球图

分析准备

分析整个水球图,波形的绘制就是整个绘制过程的最大的难点。绘制波形,第一个难点就是波纹的形状,其实可以选择两种方案绘制,第一种,采取圆弧拼接的形式;第二种就是采用三次贝塞尔曲线来绘制曲线。为了再次熟悉三次贝塞尔曲线的绘制过程,所以采用了方式二。波形的绘制难点二就是如何使得波形绘制在圆内,此处便要引入clipsaverestore三个函数的使用了,其详细情况可见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间隔绘制动画帧即可。整体上来说并没有太大的难度。