兩周前,項目里需要實現一個紅心飄飄的點贊效果。抓耳撓腮了老半天,看了幾篇大佬的文章,終於算是摸了個七七八八。不禁長嘆一聲,還是菜啊。先來看一下效果:(傳送門進去點一波)
一、Bezier曲線運動軌跡
其實用大白話描述一下需求就是讓一個紅心圖片沿着貝塞爾曲線的軌跡走,然后邊走邊消失。核心在於得到貝塞爾曲線上的一系列點。本文不會講解貝塞爾曲線的原理,因為大佬們已經講過了,而且講的比我好。參考文章如下:
其中第二篇文章講到了生成二階和三階貝塞爾曲線可以使用canvas自帶的方法:quadraticCurveTo
和bezierCurveTo
,而高階的則先得到曲線上一系列的點,然后順次連接這些點來擬合高階的貝塞爾曲線。沒錯,我們要的就是這一系列的點,有了這些點,就可以控制紅心的軌跡了。下面是我基於作者的BezierMarker.js寫的一個demo,可以直觀地看出高階貝塞爾曲線上的點:
上面100個曲線上的點坐標是由下面這段代碼計算得出的:
BezierMaker.prototype.bezier = function(t) { //貝塞爾公式調用
var x = 0,
y = 0,
bezierCtrlNodesArr = this.bezierCtrlNodesArr,
n = bezierCtrlNodesArr.length - 1,
self = this
bezierCtrlNodesArr.forEach(function(item, index) {
if(!index) {
x += item.x * Math.pow(( 1 - t ), n - index) * Math.pow(t, index)
y += item.y * Math.pow(( 1 - t ), n - index) * Math.pow(t, index)
} else {
x += self.factorial(n) / self.factorial(index) / self.factorial(n - index) * item.x * Math.pow(( 1 - t ), n - index) * Math.pow(t, index)
y += self.factorial(n) / self.factorial(index) / self.factorial(n - index) * item.y * Math.pow(( 1 - t ), n - index) * Math.pow(t, index)
}
})
return {
x: x,
y: y
}
}
這個方法就是對貝塞爾公式的實現。以3階貝塞爾公式為例(見下圖),它的方程需要四個控制點(P1,P2,P3,P4)和一個t值,就能計算出曲線上的某一點的坐標。

當將t
由0遞增到1時,就可以得到100個曲線上的點,進而擬合出相應的曲線。當我們拿到這一系列點時,其實問題已經解決了一大半了。
二、使紅心飄起來
拿到擬合點數組后,繪制軌跡就是從數組中依次拿出坐標,並將紅心圖片繪制到相應的坐標上。並根據當前擬合點在曲線數組中的位置,改變圖片的不透明度,就可以讓紅心飄起來了,上一部分代碼,講解見注釋:
// 生成隨機數
function rnd () {
let flag = Math.random() > 0.5 ? 1 : -1
return 80 * Math.random() * flag
}
class FlyHeart {
constructor (ctx, img) {
this.ctx = ctx;
this.img = heart;
// 拿到紅心的運動軌跡,一系列擬合點坐標
this.bezierArr = new BezierMaker(ctx, [
{x: 187, y: 245},
{x: 170 + rnd(), y: 200},
{x: 200 + rnd() , y: 120},
{x: 140 + rnd(), y: 60}], 90).bezierArr //90表示擬合點的數量,rnd使紅心的軌跡有一定的隨機性
}
draw () {
// 依次取出軌跡的每個點
let position = this.bezierArr.shift();
// 清除上次畫的
this.clear();
if (position) {
this.ctx.save()
// 根據當前數組長度算出透明度
this.ctx.globalAlpha = this.bezierArr.length / 30;
this.ctx.drawImage(this.img, position.x , position.y, 20, 20);
this.ctx.restore();
this.prevPosition = position;
}
}
// 清除上次畫的
clear () {
if (this.prevPosition) {
this.ctx.clearRect(this.prevPosition.x, this.prevPosition.y, 20, 20);
}
}
}
接下來就是給body添加點擊事件,當點擊時,就新生成一個紅心:
document.body.addEventListener('click', function() {
heartArr.push(new FlyHeart(ctx, heart));
})
let heartArr = []
const cvs = document.getElementById('cvs')
const ctx = cvs.getContext('2d')
const heart = document.getElementById('heart') //圖片
function draw () {
if(heartArr.length) {
for(let heart of heartArr) {
heart.draw();
if(heart.bezierArr.length === 0) {
heart.clear();
let index = heartArr.indexOf(heart)
heartArr.splice(index, 1)
}
}
}
requestAnimationFrame(draw)
}
draw()
三、后記
當時看到這個需求的時候,真的是一籌莫展,看到n階貝塞爾曲線時更是一頭霧水,但是看不懂也要看,然后看着看着,看多了也就慢慢明白了。希望沒浪費大家的時間,各位看官看完后有所收獲(完)