前言
前一陣子網(wǎng)上看到一個(gè)寫的挺好的動(dòng)畫插件,于是下載了下來。這兩天閱讀了一下代碼,然后決定分享一下原作者的思路。原效果鏈接找了半天找不到了,如果有看客留意到了可以留言,我附上鏈接(混淆后叫xs.js,可以在后面的github鏈接上下載)。
第一個(gè)效果
我在這里為了加深印象重敲了一遍,摘取部分有代表性的效果來講,首先我們看第一個(gè)效果圖:

我們所期望的是通過插件注冊(cè)一下容器即可產(chǎn)生效果,且插件擁有很多動(dòng)畫種類,首先是頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
a {
color: #999;
text-decoration: none;
}
* {
margin: 0;
padding: 0;
}
body {
background-color: #222222;
}
.box {
width: 1000px;
display: flex;
flex-wrap: wrap;
margin: 20px auto;
}
.box li {
width: 200px;
height: 100px;
background: #fff;
line-height: 100px;
margin: 20px;
text-align: center;
list-style: none;
}
.box1 {
color: blue;
}
</style>
<script type="text/javascript" src="jQuery-2.2.4.min.js"></script>
<script type="text/javascript" src="btn-ani.js"></script>
<script>
$(document).ready(function () {
$(".box1").btnHoverAni(1);
});
</script>
</head>
<body>
<div class="content">
<ul class="box">
<li class="box1"><a href="javascript:;">效果1</a></li>
</ul>
</div>
</body>
</html>
然后是插件代碼,即上面引用的btn-ani.js,注釋我添加的應(yīng)該算詳細(xì)了
(function () {
function ButtonHoverAnimation(btn, index, defaults) {
//調(diào)用者
this.btn = btn;
//調(diào)用索引,哪一個(gè)效果
this.index = index;
this.color = defaults.color;
this.init();
}
ButtonHoverAnimation.prototype = {
init: function () {
//后面的方法都使用這個(gè)obj對(duì)象的參數(shù)值
var obj = this;
obj.run = false;
if (obj.btn.css('position') != 'fixed' && obj.btn.css('position') != 'absolute') {
obj.btn.css('position', 'relative');
}
//寬高即調(diào)用者的寬高
obj.width = obj.btn.width();
obj.height = obj.btn.height();
//將調(diào)用者的子內(nèi)容層級(jí)提高,并且在最底層添加一個(gè)與調(diào)用者等寬高的畫布canvas
//這樣,我們的在canvas上的繪制作為背景不會(huì)影響到內(nèi)容的展示
obj.btn.children().each(function () {
if ($(this).css('position') != 'fixed' && $(this).css('position') != 'absolute') {
$(this).css({'position': 'relative', 'z-index': '2'});
} else if (parseInt($(this).css('z-index')) < 2) {
$(this).css({'z-index': '2'});
}
});
//背景色默認(rèn)白色
if (obj.btn.css('background-color') != "rgba(0, 0, 0, 0)") {
obj.backgroundColor = obj.btn.css('background-color');
} else {
obj.backgroundColor = '#ffffff';
}
//添加畫布,層級(jí)最低
obj.btn.append('<canvas width="' + obj.width + '" height="' + obj.height + '" style="position:absolute; top:0; left:0; z-index:1;"></canvas>');
obj.ctx = obj.btn.children('canvas')[0].getContext('2d');
//畫筆的顏色,如果調(diào)用者沒有傳入,每次鼠標(biāo)移入初始化一個(gè)隨機(jī)色
if (obj.color === false) {
obj.btn.mouseenter(function () {
obj.color = 'hsl(' + (Math.random() * 360) + ',60%,80%)';
obj.ctx.fillStyle = obj.color;
});
} else {
obj.ctx.fillStyle = obj.color;
}
//鼠標(biāo)移動(dòng)時(shí)記錄坐標(biāo)x,y
obj.btn.mousemove(function (e) {
obj.x = e.pageX - obj.btn.offset().left - parseInt(obj.btn.css('border-left-width'));
obj.y = e.pageY - obj.btn.offset().top - parseInt(obj.btn.css('border-top-width'));
});
obj.array = [];
//鼠標(biāo)移入移出時(shí),我們記錄一個(gè)mouseenter變量,并且移入時(shí)使用display方法去尋找對(duì)應(yīng)的效果
obj.btn.mouseenter(function (e) {
obj.mouseenter = true;
obj.x = e.pageX - obj.btn.offset().left - parseInt(obj.btn.css('border-left-width'));
obj.y = e.pageY - obj.btn.offset().top - parseInt(obj.btn.css('border-top-width'));
obj.display(obj);
});
obj.btn.mouseleave(function () {
obj.mouseenter = false;
});
//清屏
obj.ctx.clearRect(0, 0, obj.width, obj.height);
},
//封裝一個(gè)好畫圓的方法
drawArc: function (obj, x, y, radius, startAngle, endAngle, globalAlpha, fillStyle) {
obj.ctx.beginPath();
obj.ctx.arc(x, y, radius, startAngle, endAngle);
obj.ctx.closePath();
obj.ctx.globalAlpha = globalAlpha;
obj.ctx.fillStyle = fillStyle;
obj.ctx.fill();
},
display: function (obj) {
if (obj.index == 1) {
obj.ani1(obj);
} else {
console.warn('請(qǐng)注意,沒有第' + obj.index + '個(gè)效果??!');
}
},
ani1: function (obj) {
},
};
var defaults = {
color: false
};
$.fn.btnHoverAni = function (index, config) {
defaults = {
color: false
};
$.extend(defaults, config);
$(this).each(function () {
new ButtonHoverAnimation($(this), index, defaults);
});
}
})(jQuery);
我們通過傳入index來確定動(dòng)畫效果,在插件內(nèi)通過display方法來尋找對(duì)應(yīng)的效果并執(zhí)行,這里我們預(yù)留了一個(gè)ani1來實(shí)現(xiàn)我們的動(dòng)畫。
我們可以看到第一個(gè)效果很簡(jiǎn)單,就是鼠標(biāo)移入,圓圈放大,鼠標(biāo)移出,圓圈縮小,圓圈的最大半徑是中心到頂點(diǎn)的距離。(填充的隨機(jī)色在上面已經(jīng)寫好了,使用obj.color即可)。
ani1是我們執(zhí)行每一幀的方法,那么一些不需要一直計(jì)算的值(比如最大半徑)在display中計(jì)算好即可:
if (obj.index == 1) {
obj.radius = 0;
obj.radiusMax = Math.sqrt(Math.pow(obj.width / 2, 2) + Math.pow(obj.height / 2, 2));
if (!obj.run) {
obj.run = true;
obj.ani1(obj);
}
}
然后實(shí)現(xiàn)動(dòng)畫效果
ani1: function (obj) {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
if (obj.mouseenter) {
obj.radius += 5;
} else {
obj.radius -= 5;
}
if (obj.radius >= obj.radiusMax) {
obj.radius = obj.radiusMax;
} else if (obj.radius <= 0) {
obj.radius = 0;
}
obj.drawArc(obj, obj.width / 2, obj.height / 2, obj.radius, 0, Math.PI * 2, 1, obj.color);
if (obj.mouseenter || obj.radius > 0) {
requestAnimationFrame(function () {
obj.ani1(obj);
})
} else {
obj.radius = 0;
obj.ctx.clearRect(0, 0, obj.width, obj.height);
obj.run = false;
}
}
這樣第一個(gè)簡(jiǎn)單的動(dòng)畫就做完了,后面添加動(dòng)畫,只需要在display方法中添加分支即可。
第二個(gè)效果

第二個(gè)效果就是一個(gè)轉(zhuǎn)圈了,這里我們以長(zhǎng)方形中心為圓心,圓心到頂點(diǎn)的距離為半徑,每幀都自正上方順時(shí)針畫圓弧即可(超過canvas的內(nèi)容是看不到的)。鼠標(biāo)移入時(shí),每一幀掃過弧度自增,鼠標(biāo)移出時(shí)自減。
首先是添加一個(gè)動(dòng)畫分支:
else if (obj.index == 2) {
if (obj.array.length == 0) {
obj.radius = Math.sqrt(Math.pow(obj.width / 2, 2) + Math.pow(obj.height / 2, 2));
obj.array = [obj.width / 2, obj.height / 2, obj.radius, -90];
}
if (!obj.run) {
obj.run = true;
obj.ani2(obj);
}
}
同樣是不需要重復(fù)計(jì)算值優(yōu)先計(jì)算下來,我們記下了一個(gè)數(shù)組來確定圓弧的圓心和掃過的結(jié)束角度。然后是具體實(shí)現(xiàn):
ani2: function (obj) {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
if (obj.mouseenter) {
obj.array[3] += 15;
} else {
obj.array[3] -= 15;
}
if (obj.array[3] >= 270) {
//掃完一圈
obj.array[3] = 270;
} else if (obj.array[3] <= -90) {
//回到原位
obj.array[3] = -90;
}
obj.ctx.globalAlpha = 1;
obj.ctx.fillStyle = obj.color;
obj.ctx.beginPath();
obj.ctx.moveTo(obj.array[0], obj.array[1]);
//開始角度為-90,即圓心正上方開始
obj.ctx.arc(obj.array[0], obj.array[1], obj.array[2], -90 * Math.PI / 180, obj.array[3] * Math.PI / 180);
obj.ctx.closePath();
obj.ctx.fill();
if (obj.mouseenter || obj.array[3] > -90) {
requestAnimationFrame(function () {
obj.ani2(obj);
});
} else {
obj.array = [];
obj.ctx.clearRect(0, 0, obj.width, obj.height);
obj.run = false;
}
}
第三個(gè)效果

gif壓縮后好像有點(diǎn)掉幀,實(shí)際上的效果就是首先上下各填充至三分之一處,然后中間的三分之一左右填充。
為了描述形象一點(diǎn)我決定秀一波畫作:

那么事實(shí)上就是一開始在長(zhǎng)方塊四周畫四個(gè)長(zhǎng)方塊,上下的高度各為總高的三分之一,左右的寬度各為總寬的二分之一,我們能看到的是寬w高h(yuǎn)的區(qū)域。鼠標(biāo)移入時(shí),首先移動(dòng)上下方塊,當(dāng)各自移動(dòng)三分之一時(shí),開始移動(dòng)左右方塊,直至填滿。
else if (obj.index == 3) {
if (obj.array.length == 0) {
obj.rectHeight = Math.ceil(obj.height / 3);
obj.array[0] = {x: 0, y: -obj.rectHeight, w: obj.width, h: obj.rectHeight};
obj.array[1] = {x: 0, y: obj.height, w: obj.width, h: obj.rectHeight};
obj.array[2] = {x: -obj.width / 2, y: obj.rectHeight, w: obj.width / 2, h: obj.rectHeight};
obj.array[3] = {x: obj.width, y: obj.rectHeight, w: obj.width / 2, h: obj.rectHeight};
obj.ySpeed = obj.rectHeight / 10;
obj.xSpeed = obj.width / 20;
}
if (!obj.run) {
obj.run = true;
obj.ani3(obj);
}
}
我們記下方塊的高度三分之一,四個(gè)方塊的坐標(biāo),以及上下移動(dòng)速度ySpeed和左右移動(dòng)速度xSpeed,接著具體實(shí)現(xiàn)如下:
ani3: function (obj) {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
if (obj.mouseenter) {
obj.array[0].y += obj.ySpeed;
obj.array[1].y -= obj.ySpeed;
if (obj.array[0].y >= 0) {
obj.array[0].y = 0;
obj.array[1].y = obj.height - obj.rectHeight;
obj.array[2].x += obj.xSpeed;
obj.array[3].x -= obj.xSpeed;
if (obj.array[2].x >= 0) {
obj.array[2].x = 0;
obj.array[3].x = obj.width / 2;
}
}
} else {
obj.array[2].x -= obj.xSpeed;
obj.array[3].x += obj.xSpeed;
if (obj.array[2].x <= -obj.width / 2) {
obj.array[2].x = -obj.width / 2;
obj.array[3].x = obj.width;
obj.array[0].y -= obj.ySpeed;
obj.array[1].y += obj.ySpeed;
if (obj.array[0].y <= -obj.rectHeight) {
obj.array[0].y = -obj.rectHeight;
obj.array[1].y = obj.height;
}
}
}
obj.ctx.globalAlpha = 1;
obj.ctx.fillStyle = obj.color;
for (var i = 0; i < obj.array.length; i++) {
obj.ctx.fillRect(obj.array[i].x, obj.array[i].y, obj.array[i].w, obj.array[i].h);
}
if (obj.mouseenter || obj.array[0].y > -obj.rectHeight) {
requestAnimationFrame(function () {
obj.ani3(obj);
});
} else {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
obj.array = [];
obj.run = false;
}
}
鼠標(biāo)移入時(shí),當(dāng)?shù)谝粋€(gè)方塊的左上角坐標(biāo)y大于等于0時(shí),左右方塊開始移動(dòng)直至填滿,鼠標(biāo)移出反之即可。
第四個(gè)效果
基于第三個(gè)效果,我就又想了一個(gè)效果,就是四邊四個(gè)三角形同時(shí)移入移出,先看展示效果:

原理是一樣的,所以直接上代碼了:
else if (obj.index == 4) {
if (obj.array.length == 0) {
obj.array[0] = {x: obj.width / 2, y: 0};
obj.array[1] = {x: obj.width / 2, y: obj.height};
obj.array[2] = {x: 0, y: obj.height / 2};
obj.array[3] = {x: obj.width, y: obj.height / 2};
obj.ySpeed = obj.height / 40;
obj.xSpeed = obj.width / 40;
}
if (!obj.run) {
obj.run = true;
obj.ani4(obj);
}
}
我這里只記下四個(gè)三角形頂點(diǎn)的左邊,因?yàn)槠渌麅牲c(diǎn)的坐標(biāo)可以通過頂點(diǎn)坐標(biāo)來計(jì)算。為了同時(shí)移入,ySpeed和xSpeed根據(jù)寬高等分各自定義。
ani4: function (obj) {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
if (obj.mouseenter) {
obj.array[0].y += obj.ySpeed;
obj.array[1].y -= obj.ySpeed;
obj.array[2].x += obj.xSpeed;
obj.array[3].x -= obj.xSpeed;
if (obj.array[0].y >= obj.height / 2) {
obj.array[0].y = obj.array[1].y = obj.height / 2;
obj.array[2].x = obj.array[3].x = obj.width / 2;
}
} else {
obj.array[0].y -= obj.ySpeed;
obj.array[1].y += obj.ySpeed;
obj.array[2].x -= obj.xSpeed;
obj.array[3].x += obj.xSpeed;
if (obj.array[0].y <= 0) {
obj.array[0].y = 0;
obj.array[1].y = obj.height;
obj.array[2].x = 0;
obj.array[3].x = obj.width;
}
}
for (var i = 0; i < obj.array.length; i++) {
obj.ctx.globalAlpha = 1;
obj.ctx.fillStyle = obj.color;
if (obj.array[0].y >= obj.height / 2) {
obj.ctx.fillRect(0, 0, obj.width, obj.height);
} else {
obj.ctx.beginPath();
obj.ctx.moveTo(obj.array[i].x, obj.array[i].y);
obj.ctx.lineTo(i == 3 ? obj.array[i].x + obj.width / 2 : obj.array[i].x - obj.width / 2,
i == 1 ? obj.array[i].y + obj.height / 2 : obj.array[i].y - obj.height / 2);
obj.ctx.lineTo(i == 2 ? obj.array[i].x - obj.width / 2 : obj.array[i].x + obj.width / 2,
i == 0 ? obj.array[i].y - obj.height / 2 : obj.array[i].y + obj.height / 2);
obj.ctx.fill();
}
}
if (obj.mouseenter || obj.array[0].x > 0) {
requestAnimationFrame(function () {
obj.ani4(obj);
});
} else {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
obj.array = [];
obj.run = false;
}
}
lineTo的計(jì)算其實(shí)四個(gè)三角形分別寫清晰一點(diǎn),我這里為了縮短代碼長(zhǎng)度所以這么寫了,看著有點(diǎn)亂= 。= 。
第五個(gè)效果

這個(gè)效果就是隨機(jī)畫圓,我們用一個(gè)數(shù)組去存儲(chǔ)這些圓,每次繪制數(shù)組中所有的圓,然后每個(gè)圓的透明度不斷變小,半徑不斷變大,當(dāng)透明度減到0以下時(shí),從數(shù)組中移除這個(gè)圓。圓心的位置通過隨機(jī)數(shù)來確定,即Math.random() 乘以obj.width和Math.random() 乘以obj.height
下面看代碼:
else if (obj.index == 5) {
if (obj.array.length == 0) {
obj.ani5(obj);
}
}
ani5: function (obj) {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
//第二個(gè)條件是為了控制產(chǎn)生速率
if (obj.mouseenter && Math.random() < .5) {
obj.array.push({
x: Math.random() * obj.width,
y: Math.random() * obj.height,
radius: 1,
alpha: 1,
color: obj.color
})
}
for (var i = 0; i < obj.array.length; i++) {
obj.drawArc(obj, obj.array[i].x, obj.array[i].y, obj.array[i].radius, 0, 2 * Math.PI, obj.array[i].alpha, obj.array[i].color);
obj.array[i].alpha -= .02;
obj.array[i].radius += .4;
if (obj.array[i].alpha <= 0) {
obj.array.splice(i, 1);
i--;
}
}
obj.ctx.globalAlpha = 1;
if (obj.mouseenter || obj.array.length > 0) {
requestAnimationFrame(function () {
obj.ani5(obj);
})
} else {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
}
}
第六個(gè)效果

這個(gè)效果即在鼠標(biāo)的位置產(chǎn)生隨機(jī)向四周移動(dòng)的圓圈,主要的技術(shù)點(diǎn)就在于圓圈隨機(jī)移動(dòng)的方向了,所以只能由我靈魂畫師來解釋了:

假設(shè)圓圈在p1點(diǎn),下一幀移動(dòng)到p2點(diǎn),移動(dòng)距離為1,假設(shè)角度為a,那么p2點(diǎn)的坐標(biāo)即為x1+cosa,y1+sina。這樣下來,只要角度取一個(gè)隨機(jī)值,即可實(shí)現(xiàn)向四面八方擴(kuò)散的效果。需要注意是的移除圓圈的條件,因?yàn)閳A圈完全移動(dòng)出方塊,是需要考慮圓圈的半徑的。
代碼如下:
else if (obj.index == 6) {
if (obj.array.length == 0) {
obj.ani6(obj);
}
}
ani6: function (obj) {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
if (obj.mouseenter) {
obj.radius = Math.random() * 3 + 1;
obj.direction = Math.random() * Math.PI * 2;
obj.array.push({x: obj.x, y: obj.y, radius: obj.radius, direction: obj.direction})
}
for (var i = 0; i < obj.array.length; i++) {
obj.drawArc(obj, obj.array[i].x, obj.array[i].y, obj.array[i].radius, 0, 2 * Math.PI, 0.6, obj.color);
obj.array[i].x += Math.cos(obj.array[i].direction);
obj.array[i].y += Math.sin(obj.array[i].direction);
//注意移除圓圈的條件需要考慮半徑
if (obj.array[i].y > obj.array[i].radius + obj.height || obj.array[i].y < -obj.array[i].radius || obj.array[i].x < -obj.array[i].radius || obj.array[i].x > obj.array[i].radius + obj.width) {
obj.array.splice(i, 1);
i--;
}
}
obj.ctx.globalAlpha = 1;
if (obj.mouseenter || obj.array.length > 0) {
requestAnimationFrame(function () {
obj.ani6(obj);
})
} else {
obj.ctx.clearRect(0, 0, obj.width, obj.height);
}
}
最后我們?cè)賮砜催@樣一個(gè)效果:

可以看到,這個(gè)效果實(shí)際上也就是上兩個(gè)效果加在一起而已,就如何實(shí)現(xiàn)抖動(dòng)而言,上個(gè)效果方向是固定的,我們這里移動(dòng)的xy都取隨機(jī),如下:
obj.array[i].x += Math.cos(Math.random() * 2 * Math.PI);
obj.array[i].y += Math.sin(Math.random() * 2 * Math.PI);
或者兩個(gè)取相同的隨機(jī)角度也行,都可以實(shí)現(xiàn)抖動(dòng)的效果。
結(jié)語
效果還有很多就不多說了,有興趣的同學(xué)可以再去研究研究。但是只要明白原理,再復(fù)雜的動(dòng)畫拆解了我們都可以明了。
博主對(duì)于前端的動(dòng)畫比較感興趣,一些比較酷炫的動(dòng)畫以后也會(huì)持續(xù)更新,這里先激勵(lì)自己一波。
附上演示地址:
http://jsrun.net/pPYKp
github地址:
https://github.com/bestneville/JqueryBtnHoverAni