又双叒叕是一个 canvas 动画

栏目: Html5 · 发布时间: 5年前

内容简介:这个动画效果是在网易云音乐 app 上看到的,抱着复习 canvas 的心态来实现。现在开始分析并且实现所需要的功能根据效果,我们能初步想到的需要设置的参数如下,参数的具体值可以在随后实现中进一步修改

这个动画效果是在网易云音乐 app 上看到的,抱着复习 canvas 的心态来实现。

现在开始分析并且实现所需要的功能

列出需求

  1. 效果参照已实现的上方链接。
  2. 针对不同使用场景需要可定制参数(大小,颜色等)
  3. 需要根据音乐节奏快慢进行动画

开始实现

1. 参数设置

根据效果,我们能初步想到的需要设置的参数如下,参数的具体值可以在随后实现中进一步修改

const originParams = {
  cover: '',   // 中心的封面图
  size: 500,  // 画布 canvas 的尺寸
  radius: 100,  // 封面图,中心圆的半径,小于零则为容器的百分比
  interval: [500, 1500],  // 涟漪出现的最小频率(毫秒)
  centerColor: '#ddd',  // 封面图位置的颜色(在没有封面图时显示)
  borderWidth: 5,  //  封面图边框的宽度
  borderColor: '#aaa',  // 封面图边框的颜色
  rippleWidth: 4,  // 涟漪圆环的宽度
  rippleColor: '#fff',  // 涟漪颜色
  pointRadius: 8,  // 涟漪圆点的半径
  rotateAngle: .3, // 封面图每帧旋转的角度
}
复制代码

2. 编写构造函数

我们知道,动画的原理,就是趁脑子不注意反应不过来的时候偷偷换上一副差不多的画面,这样一步一步的替换,就形成了动画,而这一步一步就可以叫做

所以,在渲染圆圈及圆点时,我们需要一个数组来存储他们每一次渲染的位置。

另外,我们需要一些必要的初始化判断,以及声明一些公共的参数

综上,我们基本可以写出如下的构造函数

class Ripple {
  constructor(container, params = {}) {
    const originParams = {
      cover: '',
      size: 500, 
      radius: 100, 
      interval: [500, 1500], 
      centerColor: '#ddd', 
      borderWidth: 5, 
      borderColor: '#aaa', 
      rippleWidth: 4, 
      rippleColor: '#fff', 
      pointRadius: 8, 
      rotateAngle: .3, 
    }

    this.container = typeof container === "string" ? document.querySelector(container) : container

    this.params = Object.assign(originParams, params)

    this.cover = this.params.cover

    this.radius = this.params.radius < 1 ? this.params.size * this.params.radius : this.params.radius

    this.center = this.params.size / 2  // 中心点

    this.rate = 0  // 记录播放的帧数
    this.frame = null  // 帧动画,用于取消
    this.rippleLines = []  // 存储涟漪圆环的半径
    this.ripplePoints = []  // 存储涟漪点距离中心点的距离
  }
}
复制代码

3. 初始化容器

canvas 中图片渲染且旋转并不容易,所以在 cover 参数传值时,通过 img 标签来渲染。

另外的,我们需要一些其他必要的 CSS 添加在元素上

class Ripple{
    initCanvas() {
      this.container.innerHTML = `<canvas width="${this.params.size}" height="${this.params.size}"></canvas>${this.cover ? `<img src="${this.cover}" alt="">` : ''}`
  
      this.cover = this.container.querySelector('img')
      this.canvas = this.container.querySelector('canvas')
      this.ctx = this.canvas.getContext('2d')
  
      this.rotate = 0
  
      const containerStyle = { ... }
      const canvasStyle = { ... }
      const coverStyle = { ... }
  
      utils.addStyles(this.container, containerStyle)
      utils.addStyles(this.canvas, canvasStyle)
      utils.addStyles(this.cover, coverStyle)
  
      this.strokeBorder()
    }
  }
复制代码

4. 画个圆

canvas 的基本用法,就不多赘述了

class Ripple{
  strokeCenterCircle() {
    const ctx = this.ctx
    ctx.beginPath()
    ctx.arc(this.center, this.center, this.radius, 0, 2 * Math.PI)
    ctx.closePath()
    ctx.fillStyle = this.params.centerColor
    ctx.fill()
   }
  strokeBorder() {
    const ctx = this.ctx
    ctx.beginPath()
    ctx.arc(this.center, this.center, this.radius + this.params.borderWidth / 2, 0, 2 * Math.PI)
    ctx.closePath()
    ctx.strokeStyle = this.params.borderColor
    ctx.lineWidth = 5
    ctx.stroke()
  }
}
复制代码

5. 画个圈圈

这不就是一个圈加一个圆吗

class Ripple{
      drawRipple() {
        const ctx = this.ctx
  
        // 画外圈
        ctx.beginPath()
        ctx.arc(this.center, this.center, 200, 0, Math.PI * 2)
        ctx.strokeStyle = 'rgba(255,255,255,0.4)'
        ctx.lineWidth = this.params.rippleWidth
        ctx.stroke()
  
        // 画点
        ctx.beginPath()
        ctx.arc(this.center - 200/Math.sqrt(2), this.center - 200/Math.sqrt(2), this.params.pointRadius, 0, 2 * Math.PI)
        ctx.closePath()
        ctx.fillStyle = 'rgba(255,255,255,0.4)'
        ctx.fill()
      }
    }
复制代码

于是出现了下面的问题

又双叒叕是一个 canvas 动画

出现的原因也很简单,两个半透明的图形的重合部分透明度肯定是会加重的,所以只能通过画一个不完整的圆圈(正好把圆点的部分隔过去)来解决了,解决方式如下图:

又双叒叕是一个 canvas 动画

为了容易看到,连线略微向内移动了一点,所以我们的问题就是已知 r、R,求角度 θ,解答就不做详解啦,算是高中数学的应用。我们可以得到角度为 Math.asin(R / r / 2) * 4

6. 继续画圈圈

圆环和圆点的重合已经解决了,现在需要的是在每次刷新时更新他们的位置,如果达到了条件,则需要新添加一个圆环和点,如果圆环的半径超出了画布,则删掉对应的数据

class Ripple{
    strokeRipple() {
      // 当圆环大小超出画布时,删除改圆环数据
      if (this.rippleLines[0] > this.params.size) {
        this.rippleLines.shift()
        this.ripplePoints.shift()
      }
  
      // 当达到条件时,添加数据
      if (this.rate - this.lastripple >= this.minInterval) {
        this.rippleLines.push({
          r: this.radius + this.params.borderWidth + this.params.rippleWidth / 2,
          color: utils.getRgbColor(this.params.rippleColor)
        })
  
        this.ripplePoints.push({
          angle: utils.randomAngle()
        })
        // 更新添加时间
        this.lastripple = this.rate
      }
  
      // 计算下一次渲染的位置数据
      this.rippleLines = this.rippleLines.map((line, index) => ...)
  
      this.ripplePoints = this.rippleLines.map((line, index) => ...)
  
      // 根据新的数据渲染
      this.strokeRippleLine()
      this.strokeRipplePoint()
    }
  }
复制代码

6. 开始动画

每渲染一次即更新一次数据,将 requestAnimationFrame 存储于 this.frame 中,方便取消。

class Ripple{
    animate() {
      this.ctx.clearRect(0, 0, this.params.size, this.params.size)
  
      this.strokeRipple()
  
      this.strokeBorder()
  
      ...
  
      var that = this
      this.frame = requestAnimationFrame(function () {
        that.animate()
      })
    }
}
复制代码

7. 添加音乐节奏

原理如下:

  1. 创建一个 <audio> ,做好相关设置(自动播放、控件显示等等)
  2. 创建一个 AudioContext,将音频来源设置为这个
  3. 创建一个 AnalyserNode,将 AnalyserNode 与 AudioContext 的音频来源链接起来
  4. 如果需要达到一边播放一遍显示的效果,需要将 Analyser 再连出 AudioContext,否则数据只进不出没有声音
  5. 创建一个可以绘制的地方(可以是普通的 HTML元素,或者 canvas,SVG,WebGL等等)
  6. 定时从 AnalyserNode 获取音频数据(可以是时间域或频率域的数据,定时可以用 requestAnimationFrame 或者是 setTimeout 来做)
  7. 利用获取到的数据,进行可视化设计,绘制图像

但是,可能是 audiocontext 的兼容问题,在 safari 中,无法实时获取到音频信息,如果有大神知晓,望不吝赐教。

所以这部分的实现并没有什么好讲的了,有兴趣的可以直接查看 源码 及实现

写在最后

上述动画的实现的确并不复杂,但是在实现的过程中,可能考虑到的更多的是如何组织代码,如何设计接口(怎么方便使用,增加定制度,减少操作度)。这些东西在写上面教程的时候轻猫淡写,一笔带过或者压根没提过,但只有在自己写时才能体会到切实的需要,所以,如果你能看到这里,不妨放下刚才看到的代码实现,只看效果,自己也来写一个(带音乐的)这样的动画,毕竟,我们不缺少发现简单的眼睛,缺少的可能是完美简单的手。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

百度SEO一本通

百度SEO一本通

潘坚、李迅 / 电子工业出版社 / 2015-6 / 59.00元

《百度SEO一本通》通过浅显易懂的叙述方式,以及大量的图示,详细介绍了SEO的关键技术要点,对于搜索引擎优化中重要的关键词优化、链接优化,以及百度推广中的推广技巧都进行了详细的介绍。 《百度SEO一本通》共分为11章,首先让大家了解SEO存在的原因,然后对网页、网站、空间和程序与SEO的关系展开了细节上的讨论,最后几章深入介绍了百度推广的相关概念、设置、技巧和实操,让读者可以轻松上手操作,易......一起来看看 《百度SEO一本通》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具