装逼速成之手把手教你画 Wave 动画

1,408 阅读5分钟

昨天晚上睡觉前脑海中突然闪过一个特效,从构思,设计,编码,调试,优化,雕琢花了一个多小时。自己爽了一把,发给烧饼哥和吴大狗看,得到了一致评价。

先看效果图:

wave

wave

由于录屏的缘故,看上去可能有些卡顿,也可以打开 CodePen 看源码,接下来的内容主要是讲如何实现这样的效果。

(当然,我知道无论发什么给他们得到的结果都是一样的牛逼Orz,以至于有时候我不能确定他们有没有点开链接看。)

日常Orz

日常Orz

Intro

很早以前,刚开始学前端,买了本 CSS + HTML 入门实战之类的书,然后设计师甩给我一个酷炫的曲线图。

请问这样的效果能实现吗?
额。。。我看一下。。。嗯。。。不好看吧,太花哨了。。。要不你看看 iChart 的图表怎么样。。。有现成的,不要重复造轮子了。。。

相信不少人有跟我类似的经历,面对狂拽酷炫的动画无从下手,就像我第一次看到 Wave 动画的时候,心里冒出千千万万个疑问,卧槽,这里的曲线是怎么实现的,流畅的动画是怎么实现的,渐变又TM是怎么实现???

一次次经历接受设计和产品的拷问,你也不好意思所有需求都拿 iChart 来挡箭了。动手吧骚年。

练兵场

像极了每一次初始化在线演示项目,得想好用到什么方式实现。这种酷炫的效果 DOM 节点肯定是不合适了,SVG 兴许能实现,但是能预见到必然会有明显卡顿。用 Canvas 吧。

可以通过 js 来插入 canvas,可以不写 html,那就直接写点样式意思意思。

html,
body {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}

canvas {
  width: 100%;
  height: 100%;
  background: #232323;
}

把基础的代码准备好,然后再 requestAnimationFrame 里面来实现每一帧的动画,当然,如果你不需要动画,直接把 requestAnimationFrame 去掉就行了。

const canvas = document.createElement('canvas')
document.body.appendChild(canvas)

const ctx = canvas.getContext('2d')

const width = canvas.width = canvas.offsetWidth
const height = canvas.height = canvas.offsetHeight

const run = () => {
  requestAnimationFrame(run)
  ctx.clearRect(0, 0, width, height)
  ctx.beginPath()
  ctx.strokeStyle="white"
  ctx.lineWidth = 1
  ctx.moveTo(0, height * 0.5)
  ctx.lineTo(width, height * 0.5)
  ctx.stroke()
  ctx.closePath()
}

run()

不出意外,你可以在屏幕水平中看到一条白色线条,继续继续。

性感的三角函数

实现 Wave 动画的基础及核心就是三角函数。我们对这个家伙再了解不过了,只是在实际用到的时候,没想起来罢了。

sin & cos

sin & cos

简单回顾一下,高中在解数学题的时候如鱼得水的快感,应该能记起来不少三角函数的定理吧。我们不展开讲三角函数,不然就变成乏味的大学课堂了。

反正连续的 x 通过 Math.sin 就能映射到正弦函数上的值就对了,连起来就是一条曲线啊。( 大家好我是谷阿莫)

const run = () => {
  // ...
  ctx.moveTo(0, height * 0.5)
  for (let x = 0; x < width; x++) {
    const y = Math.sin(x) + height / 2
    ctx.lineTo(x, y)
  }
  ctx.stroke()
  ctx.closePath()
}

仔细看,直线变成了正弦曲线。

正弦曲线

正弦曲线

不够明显,调整下参数。按照高中的思维是,先放大横坐标,再提高振幅~噢对了,顺便加点渐变色。


const grad = ctx.createLinearGradient(0, 0, width, 0);
grad.addColorStop(0, '#6e45e2');
grad.addColorStop(1, '#88d3ce');
ctx.strokeStyle = grad
ctx.lineWidth = 1
ctx.moveTo(0, height * 0.5)
for (let x = 0; x < width; x++) {
  const y = Math.sin(x * 0.02) * 50 + height / 2
  ctx.lineTo(x, y)
}

单条渐变曲线

单条渐变曲线
🎉🎉🎉 恭喜你完成任务一。

浪起来

接下来的任务是让曲线动起来。

那怎么让曲线平滑地动起来呢?事实上,动画形成的原理是以一定的速度(如每秒16张)连续播放时,人眼捕获到之后通过视觉残象产生错觉,把它们连起来。所以!!!连续移动曲线就形成能动画…

前面提到 requestAnimationFrame ,这个好家伙(microTask)会在每次浏览器合适的时候执行回调(能很好满足我们对帧率的要求),不像 setTimeout (macroTask)容易出现掉帧,跳动的问题。

调整一下代码。

let xMove = 0
let xSpeed = -0.04

const run = () => {
  // ...
  xMove += xSpeed
  for (let x = 0; x < width; x++) {
    const y = Math.sin(x * 0.02 + xMove) * 50 + height / 2
    ctx.lineTo(x, y)
  } 
  // ...
}

现在你得到了一条绵延不断,并且带渐变的正弦曲线。由于所有波峰波谷都一样高,有点像心跳检测仪器。。。再想办法让入口和出口的振幅减小。这时候三角函数又派上用场。很明显,只要让正弦曲线向右移动 0.5 * PI,然后再向上移动 1 就可以得到我们要的效果,左半个周期是从 0 -> 1 右半个周期是从 1 -> 0。

sine变形

sine变形

for (let x = 0; x < width; x++) {
  const scale = (Math.sin(x / width * Math.PI * 2 - Math.PI * 0.5) + 1) * 0.5
  const y = Math.sin(x * 0.02 + xMove) * 50 * scale + height / 2
  ctx.lineTo(x, y)
}

就这么简单,再稍微处理一下,支持多条曲线同时展示。就大工告成!由于时间关系,我想睡觉了。👋 完整代码看Codepen,欢迎 Star !

wave

wave

最后感谢 Chinuketsu,没有她,我早就写完了。

最后感谢 Chinuketsu,没有她,我早就写完了。

最后感谢 Chinuketsu,没有她,我早就写完了。

相关链接

如需转载,请注明出处: w3ctrain.com / 2018/06/26/wave-step-by-step/

我叫周晓楷

我现在是一名前端开发工程师,在编程的路上我还是个菜鸟,w3ctrain 是我用来记录学习和成长的地方。