本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
摸鱼的时候刷到这种治愈风格的短视频,就突发奇想用代码实现一下。
心形线方程
U1S1,数学真的很奇妙,也很佩服那帮搞数学的,我随便在网上找了个
x = 16 * pow(sin(t), 3)
y = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)
直接用打点的方式画出来,效果还挺不错
双向奔赴
看了第一种的效果,很显然可以优化一下绘制的方式,用2条路径来实现双向奔赴效果,一条从0到180
,一条从360到180
let d = 0
let d2 = 360
let timer2 = setInterval(() => {
if(d < 180) {
d += 1
let x = 16 * Math.pow(Math.sin(d * Math.PI / 180), 3)
let y = 13 * Math.cos(d * Math.PI / 180) - 5 * Math.cos(2 * d * Math.PI / 180) - 2 * Math.cos(3 * d * Math.PI / 180) - Math.cos(4 * d * Math.PI / 180)
ctx.fillRect(x * 5 + 300, y * 5 - 100, 2, 2)
}
if(d2 > 180) {
d2 -= 1
let x = 16 * Math.pow(Math.sin(d2 * Math.PI / 180), 3)
let y = 13 * Math.cos(d2 * Math.PI / 180) - 5 * Math.cos(2 * d2 * Math.PI / 180) - 2 * Math.cos(3 * d2 * Math.PI / 180) - Math.cos(4 * d2 * Math.PI / 180)
ctx.fillRect(x * 5 + 300, y * 5 - 100, 2, 2)
}
}, 20);
花瓣爱心
到现在已经有了心形线轨迹,用花瓣替换轨迹上的点,就可以得到一个花瓣爱心了
先准备一个花瓣类,随机大小和种类
class Flower {
constructor(option) {
super(option)
this.image = new Image()
this.w = option.w
this.h = option.h
this.image.src = `./images/${[1, 2, 3, 4, 5, 6][Math.floor(Math.random() * 6)]}.png`
}
draw(ctx) {
ctx.drawImage(this.image, this.x, this.y, this.w, this.h);
}
}
云朵爱心
我们最终要实现的是飞机运动轨迹产生云朵爱心,初步设想是飞机绕心形线轨迹运动,飞机的尾部生成云朵,模拟上面的效果,先用云朵图片替换花瓣,看一下效果
这个时候就发现了两个问题
- 正常来讲飞机扰动产生的云朵,应该是由小到大扩散
- 飞机飞行是有转向的
我们先来尝试解决第一个问题,每个云朵初始化后加上一个由小到大的变化过程
花瓣生长爱心
我们在花瓣爱心的基础上进行改造,花瓣初始化后,增加一个由小到大的渐变动画
class Flower {
constructor(option) {
super(option)
this.image = new Image()
this.w = option.w
this.h = option.h
this.image.src = `./images/f${[1, 2, 3, 4, 5, 6][Math.floor(Math.random() * 6)]}.png`
this.c = 0.02
this.grow()
}
draw(ctx) {
ctx.drawImage(this.image, this.x, this.y, this.w * this.c, this.h * this.c);
}
grow() {
this.timer = new Timer(() => {
if(this.c <= 1) {
this.c += 0.02
} else {
this.timer.clear()
}
},16)
}
}
飞行轨迹爱心
再来解决第二个问题,飞机在飞行的过程中,是要不断的转变方向的,为了将问题简化,我准备实现一个组合,组合沿着轨迹方程运动,内部的飞机旋转角度,如图,飞机在某一时刻的位置为A,旋转角度为45度,那么云朵产产生的位置应该为B
至于飞机的旋转角度如何跟轨迹结合起来呢,这里简单的用某一段弧线的切线来确定旋转角度,即某点的旋转角度为当前点与下一点的连线角度(由于心形线上的点的密集程度不一,可能导致不平滑) 如图,在a点的方向为a-b,b点的方向为b-c
// 1 确定当前点
let vector = {
x: plane.x,
y: plane.y
}
// 2 拿到下一个点
theta -= 2
let point = getHeartLinePoint(theta, 10)
// 3 更新飞机的位置
plane.x = point.x
plane.y = point.y
// 4 计算出向量角度并设置飞机转向
let vector2 = {
x: plane.x,
y: plane.y
}
let angle = getVectorAngle(vector, vector2)
plane.setPlaneAngle(angle)
可以看到飞机几乎是贴合心形线的轨迹转向飞行的,但是由于初始弧度太密集,后续弧度太分散了(变幻不均匀,优化空间很大),导致转向动画有点不流畅,不过效果完全达到了
飞机云朵爱心
这个时候再把云朵的生成逻辑与飞机的飞行逻辑结合,应该就能勉强达到效果了
这个时候又发现新的问题了
- 飞机转向不流畅,这个是由于弧度计算不均匀导致
- 云朵轨迹效果不像真的,这个是由于素材问题,还有一个就是云朵的渐变效果
- 云朵产生的位置不准确,这个是由于我直接取了飞机运动的轨迹,没有去计算飞机尾部的位置
- 飞机转向没有动画过渡,太快了,导致不平滑
- 云朵的渐变中心是从左上角变化的,不是中心,再由于2,导致云朵与飞机偏离很远
- 一张好看的背景图
中心渐变
先来处理云朵渐变中心点的问题,让云朵生成之后,从中心由小变大,并且加上帧动画
grow() {
if(this.sizeScale >= 1) {
this.sizeScale -= 0.1 // 初始缩小5倍,后续缩小倍数一直减小直到1倍
// 则花的offsetXY更新
this.offsetX = this.parent.w / 2 - this.w / this.sizeScale / 2
this.offsetY = this.parent.h / 2 - this.h / this.sizeScale / 2
} else {
Timer.removeElm(this.growTimer)
}
}
绿框的中心点就是花瓣的位置,我们给花瓣加上了帧动画和由小到大的效果,用这个花瓣去替换轨迹上的,看看效果如何
飞行云爱心
把花瓣换成云朵
代码
结语
这样不同方式绘制爱心的效果就完成了,不过优化空间还很大
- 素材,好的素材再加上细节处理会有更好的效果
- 飞机转向过渡动画
- 云朵的疏密和位置
- 动画的时间系数