前言
前一阵子网上看到一个写的挺好的动画插件,于是下载了下来。这两天阅读了一下代码,然后决定分享一下原作者的思路。原效果链接找了半天找不到了,如果有看客留意到了可以留言,我附上链接(混淆后叫xs.js,可以在后面的github链接上下载)。
第一个效果
我在这里为了加深印象重敲了一遍,摘取部分有代表性的效果来讲,首先我们看第一个效果图:
我们所期望的是通过插件注册一下容器即可产生效果,且插件拥有很多动画种类,首先是页面:
<!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,注释我添加的应该算详细了
(function () {
function ButtonHoverAnimation(btn, index, defaults) {
//调用者
this.btn = btn;
//调用索引,哪一个效果
this.index = index;
this.color = defaults.color;
this.init();
}
ButtonHoverAnimation.prototype = {
init: function () {
//后面的方法都使用这个obj对象的参数值
var obj = this;
obj.run = false;
if (obj.btn.css('position') != 'fixed' && obj.btn.css('position') != 'absolute') {
obj.btn.css('position', 'relative');
}
//宽高即调用者的宽高
obj.width = obj.btn.width();
obj.height = obj.btn.height();
//将调用者的子内容层级提高,并且在最底层添加一个与调用者等宽高的画布canvas
//这样,我们的在canvas上的绘制作为背景不会影响到内容的展示
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'});
}
});
//背景色默认白色
if (obj.btn.css('background-color') != "rgba(0, 0, 0, 0)") {
obj.backgroundColor = obj.btn.css('background-color');
} else {
obj.backgroundColor = '#ffffff';
}
//添加画布,层级最低
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');
//画笔的颜色,如果调用者没有传入,每次鼠标移入初始化一个随机色
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;
}
//鼠标移动时记录坐标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 = [];
//鼠标移入移出时,我们记录一个mouseenter变量,并且移入时使用display方法去寻找对应的效果
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);
},
//封装一个好画圆的方法
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('请注意,没有第' + obj.index + '个效果!!');
}
},
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来确定动画效果,在插件内通过display方法来寻找对应的效果并执行,这里我们预留了一个ani1来实现我们的动画。
我们可以看到第一个效果很简单,就是鼠标移入,圆圈放大,鼠标移出,圆圈缩小,圆圈的最大半径是中心到顶点的距离。(填充的随机色在上面已经写好了,使用obj.color即可)。
ani1是我们执行每一帧的方法,那么一些不需要一直计算的值(比如最大半径)在display中计算好即可:
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);
}
}
然后实现动画效果
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;
}
}
这样第一个简单的动画就做完了,后面添加动画,只需要在display方法中添加分支即可。
第二个效果
第二个效果就是一个转圈了,这里我们以长方形中心为圆心,圆心到顶点的距离为半径,每帧都自正上方顺时针画圆弧即可(超过canvas的内容是看不到的)。鼠标移入时,每一帧扫过弧度自增,鼠标移出时自减。
首先是添加一个动画分支:
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);
}
}
同样是不需要重复计算值优先计算下来,我们记下了一个数组来确定圆弧的圆心和扫过的结束角度。然后是具体实现:
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;
}
}
第三个效果
gif压缩后好像有点掉帧,实际上的效果就是首先上下各填充至三分之一处,然后中间的三分之一左右填充。
为了描述形象一点我决定秀一波画作:
那么事实上就是一开始在长方块四周画四个长方块,上下的高度各为总高的三分之一,左右的宽度各为总宽的二分之一,我们能看到的是宽w高h的区域。鼠标移入时,首先移动上下方块,当各自移动三分之一时,开始移动左右方块,直至填满。
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);
}
}
我们记下方块的高度三分之一,四个方块的坐标,以及上下移动速度ySpeed和左右移动速度xSpeed,接着具体实现如下:
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;
}
}
鼠标移入时,当第一个方块的左上角坐标y大于等于0时,左右方块开始移动直至填满,鼠标移出反之即可。
第四个效果
基于第三个效果,我就又想了一个效果,就是四边四个三角形同时移入移出,先看展示效果:
原理是一样的,所以直接上代码了:
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);
}
}
我这里只记下四个三角形顶点的左边,因为其他两点的坐标可以通过顶点坐标来计算。为了同时移入,ySpeed和xSpeed根据宽高等分各自定义。
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的计算其实四个三角形分别写清晰一点,我这里为了缩短代码长度所以这么写了,看着有点乱= 。= 。
第五个效果
这个效果就是随机画圆,我们用一个数组去存储这些圆,每次绘制数组中所有的圆,然后每个圆的透明度不断变小,半径不断变大,当透明度减到0以下时,从数组中移除这个圆。圆心的位置通过随机数来确定,即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);
//第二个条件是为了控制产生速率
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);
}
}
第六个效果
这个效果即在鼠标的位置产生随机向四周移动的圆圈,主要的技术点就在于圆圈随机移动的方向了,所以只能由我灵魂画师来解释了:
假设圆圈在p1点,下一帧移动到p2点,移动距离为1,假设角度为a,那么p2点的坐标即为x1+cosa,y1+sina。这样下来,只要角度取一个随机值,即可实现向四面八方扩散的效果。需要注意是的移除圆圈的条件,因为圆圈完全移动出方块,是需要考虑圆圈的半径的。
代码如下:
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);
}
}
最后我们再来看这样一个效果:
可以看到,这个效果实际上也就是上两个效果加在一起而已,就如何实现抖动而言,上个效果方向是固定的,我们这里移动的xy都取随机,如下:
obj.array[i].x += Math.cos(Math.random() * 2 * Math.PI);
obj.array[i].y += Math.sin(Math.random() * 2 * Math.PI);
或者两个取相同的随机角度也行,都可以实现抖动的效果。
结语
效果还有很多就不多说了,有兴趣的同学可以再去研究研究。但是只要明白原理,再复杂的动画拆解了我们都可以明了。
博主对于前端的动画比较感兴趣,一些比较酷炫的动画以后也会持续更新,这里先激励自己一波。
附上演示地址:
http://jsrun.net/pPYKp
github地址:
https://github.com/bestneville/JqueryBtnHoverAni