书接上回:【canvas】网易云音乐鲸云特效『水晶音波』的简单实现
今天,我来实现一下『孤独星球』这一动效。
这是网易云音乐的截图,结尾放我自己实现的效果
上回已经开好“局”了,所以这里就不必重复了,直接讲如何用canvas画背景的涟漪效果。
canvas
这回我细讲思路。
先定义一个空心圆,这空心圆上有个小球。
所以要有这么几个关键变量要有,
变量 | |
---|---|
空心圆 | 圆心位置、变大的速度、半径、渐变的范围(即圆最多有多大) |
小球 | 位置、移动的速度、半径. |
空心圆会变大,小球会移动,那么实现方式就简单了,我延续上次定义三角形的方式,这样定义
class Circle {
constructor(context, speed, pole, radius, range) {
this.ctx = context;
this.speed = speed;
this.pole = pole;
this.radius = radius;
this.range = range;
this.__restart();
}
__restart() {
this.r = this.r ? this.radius[0] : this.radius[1];
this.ballRadius = Math.floor(4 + Math.random() * 3);
this.ballAngle = Math.random() * PI2; // 小球的位置用角度表示,随机产生
this.ballPoint || (this.ballPoint = [0, 0]);
this.ballPoint[0] = Math.cos(this.ballAngle) * this.r; // 根据空心圆的半径计算小球的坐标
this.ballPoint[1] = Math.sin(this.ballAngle) * this.r;
}
__lerp(src, dst, coeff) {
return src + (dst - src) * coeff; // 线性函数,小学数学
}
__update() {
if (this.r - this.range > 0.0001) // 空心圆超过一定范围就会重新开始
this.__restart();
else {
this.r += this.speed;
this.ballAngle += .01;
this.ballPoint[0] = Math.cos(this.ballAngle) * this.r;
this.ballPoint[1] = Math.sin(this.ballAngle) * this.r;
this.opacity = this.__lerp(1, 0, this.r / this.range);
}
}
render() {
this.__update();
// 绘制空心圆
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = `rgba(179, 179, 179, ${this.opacity})`;
this.ctx.beginPath();
this.ctx.arc(this.pole[0], this.pole[1], this.r, 0, PI2);
this.ctx.stroke();
// 绘制小球
this.ctx.strokeStyle = `rgba(179, 179, 179, 0)`;
this.ctx.fillStyle = `rgba(179, 179, 179, ${this.opacity})`;
this.ctx.beginPath();
this.ctx.arc(this.pole[0] + this.ballPoint[0], this.pole[1] + this.ballPoint[1], this.ballRadius, 0, PI2);
this.ctx.stroke();
this.ctx.fill();
}
}
在上述代码中,会有几个值得细品的地方:
- 为什么这样计算小球的
[x, y]
位置写成这样?
this.ballPoint || (this.ballPoint = [0, 0]);
this.ballPoint[0] = Math.cos(this.ballAngle) * this.r;
this.ballPoint[1] = Math.sin(this.ballAngle) * this.r;
而不这样
this.ballPoint = [Math.cos(this.ballAngle) * this.r, Math.sin(this.ballAngle) * this.r];
这两种方法不会差太多,但我是个强迫症,后者会反复创建新的数组赋值给this.ballPoint
,我认为这是不好的,数组只需要创建一次,每次修改数组中的值就可以了。
- 为什么构造这个圆时,我传入的半径
radius
是一个数组?
其实这是为了实现错落有致。在后面的“场景”定义中会进一步解答这个问题。
紧接是定义“场景”。
class Scene {
constructor(canvas) {
this.cvs = canvas;
this.ctx = canvas.getContext('2d');
const slit = 50;
const pole = this.cvs.width / 2;
this.circleSet = [];
this.circleNum = Math.floor(pole / slit);
const range = this.circleNum * slit;
for (let i = 1; i < this.circleNum; ++i)
this.circleSet.push(new Circle(this.ctx, 1, [pole, pole], [slit, slit * i], range));
}
render() {
this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height);
this.circleSet.forEach(circle => circle.render());
}
run() {
if (!this.timer) {
this.timer = setInterval(this.render.bind(this), 25);
}
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = 0;
}
}
}
// 跑场景
const canvas = document.getElementById('background');
canvas.width = canvas.height = Math.ceil(canvas.parentNode.lastElementChild.offsetWidth * 1.68421);
const scene = new Scene(canvas);
scene.run();
其中
const slit = 50;
const pole = this.cvs.width / 2;
this.circleSet = [];
this.circleNum = Math.floor(pole / slit);
const range = this.circleNum * slit;
for (let i = 1; i < this.circleNum; ++i)
this.circleSet.push(new Circle(this.ctx, 1, [pole, pole], [slit, slit * i], range));
这部分是原理,我画了一个简易的示意图表示
当某个空心圆的半径超过range
,那就会到因Circle.__update()
方法而回到示意图中start
位置。以此实现周期。
最终效果
下篇 —— 网易云音乐鲸云动效『迷幻水波』的原理:【canvas】三阶贝塞尔曲线拟合圆