- 苏格团队
- 做者:Jason
某一天我收到了产品发来的微信消息。小X,咱们的业务如今须要一个相似加入购物车的掉落动画,通过组织的慎重考虑,这个需求就交给你了。因而便有了这篇文章。本文并无描述多少高深的技术,更多的是一些笔者在作动画时对动画原理的思考以及如何优化动画的一些思路。实现效果以下:前端
前端实现动画的方式有不少。不管是JS动画,CSS动画,Canvas动画仍是SVG动画,哪怕是GIF动画实现一个简单的抛物线都是足够的。但考虑业务场景的需求以及可玩性,最终决定使用JS来实现这个动画。浏览器
在笔者看来大多数动画效果,归根到底仍是 数学公式的应用。所谓抛物线动画也无非就是让元素的运动符合抛物线的运动轨迹。
抛物线的方程为: Y = A*X*X + B*X + C
也许你们看到这个公式有点陌生。但曾经物理老师念念有词的 L(距离) = 1/2*A(加速度)*T*T(时间)
想必你们都必定熟记于心。笔者正是利用这个公式来完成抛物线动画。bash
因为业务自己的特殊性,须要在用户点击物品时获取到该元素在窗口中的绝对位置。即元素相对于浏览器可见区域的X, Y的坐标。 这里笔者推荐使用getBoundingClientRect()
函数结合具体业务计算绝对位置。 固然,在一些场景里你能够直接使用鼠标点击位置或者其余任意方法获取动画的起点。微信
加速度A决定了元素在设定方向(下文都用垂直方向代替)的速度变化快慢。当动画的起点和终点都固定时,由公式 L(垂直距离) = 0.5 * A(加速度) * T * T(时间)
可得出此时 加速度A与时间T的平方成反比。
须要注意的是,正的加速度A会一直扩大帧与帧之间的垂直移动距离,因此过大的加速度A可能会致使 动画的末期小球有闪烁感。app
在抛物线的动画中,通常的咱们认为元素的 水平移动速度固定。那么一样由公式 L(水平距离) = T * Xspeed(水平速度)
可得出水平速度Xspeed实际上决定了动画的执行时长。
综合加速度的概念,咱们能够得出如下结论:
当动画的起始点和结束点必定时,若咱们设定X轴的初速度为固定值,则动画的执行时长被固定,此时为了让小球达到既定位置。加速度A须要计算生成。 具体计算公式以下:函数
// 肯定动画起始点和终点
let XStart = 0, YStart = 0, XEnd = 1000, YEnd = 1000;
// 肯定关键参数
let Xspeed = XX;
// 根据关键参数Xpeed计算动画时间与垂直加速度
let Time = (XEnd - XStart) / Xspeed;
let A = 2 * (YEnd - YStart ) / (Time * Time);
复制代码
若是须要动画执行的时长固定呢?oop
// 肯定关键参数
let Time = XX;
// 根据关键参数Time计算水平速度与垂直加速度
let Xpeed = (XEnd - XStart) / Time;
let A = 2 * (YEnd - YStart ) / (Time * Time);
复制代码
若是须要加速度固定呢?
不,你不须要 .....优化
Y轴的初速度,即抛物线抛出时垂直速度。通常的我会设置 Y轴初速度为负值。 此时会有向上抛而后天然下落的动画,略生动... 这时加速度A的计算公式变为:动画
let A = 2 * (YEnd - YStart - Yspeed * Time) / (Time * Time);
复制代码
这里须要注意的是,设定不一样比值的Xspeed 与 Yspeed能够 改变曲线的形态。背后原理为:Yspeed和加速度A(有可能受Xspeed控制)共同决定了抛出小球后小球上升阶段能达到的 最高点, 而Xspeed决定了此时的X轴位置。ui
常规的JS动画,咱们通常使用 setTimeOut 或 requestAnimationFram去实现 。下面咱们以requestAnimationFram实现 固定动画执行时长 为例。
// 起点和终点请自由设定
let XStart = 0, YStart = 0, XEnd = 1000, YEnd = 1000;
let Time = T;
let Xpeed = (XEnd - XStart) / Time;
let Ypeed = -YY;
let A = 2 * (YEnd - YStart - Yspeed * Time) / (Time * Time);
// 生成元素
let Node = document.createElement('div');
// 自由控制形体,定位通常设定为Fixed
Node.className = 'myNode';
document.body.appendChild(Node);
Node.style.top = YStart + 'px';
Node.style.left = XStart + 'px';
复制代码
// 记录元素实时位置
let nowX = XStart;
let nowY = YEnd;
// 单位时间
let loop = 0;
//
let move = () => {
if (nowY >= targetTop) {
// 销毁实例的判断可自行设定
Node.remove();
return;
}
// 当前位置等于原始位置 + 单位时间内的位移
nowX += Xspeed;
//
nowY += (A * loop + Yspeed);
requestAnimationFram(() => {
Node.style.top = nowY + 'px';
Node.style.left = nowX + 'px';
loop++;
move();
});
};
复制代码
根据中止动画的代码逻辑,小球在最后一次位移时,也许会超越咱们设定的目的点。在下一次setTimeOut的判断中咱们才会中止动画和销毁实例。解决方式以下。
requestAnimationFram(() => {
Node.style.top = Math.min(nowY, XEnd) + 'px';
Node.style.left = Math.min(nowX, YEnd) + 'px';
loop++;
move();
});
复制代码
顺便一提:这里利用Math.min()或Math.max()能够实现不少有趣的动画,本身去发现新大陆吧。
动态模糊,这里采用百度百科对其的定义 动态模糊或运动模糊(motion blur)是静态场景或一系列的图片像电影或是动画中快速移动的物体形成明显的模糊拖动痕迹。
笔者理解就是视觉信息的残留,即当前时刻的视觉来源(好比图片,视频,脑补)中残留有上一时刻的视觉信息。 这样有什么好处呢?适当的动态模糊会使连续的画面变化 变得更加流畅和天然。
首先咱们作个排除法,确定是不能放电影的...
让咱们在看一遍动态模糊实现的效果形成明显的模糊拖动痕迹
。 也就是说若是实现了模糊拖动的痕迹就能够模仿动态模糊效果。 那么模糊拖动又是什么效果呢?
笔者认为,动态模糊的效果可模拟为 在元素周围添加数个透明度渐变的相同元素
在代码实现以前,咱们在首先要肯定咱们须要实现的目标。以抛物线动画中的小球为目标,即 在运动的小球周围生成数个透明度渐变的小球。具体添加小球的位置呢?笔者的想法是,在小球俩帧位置之间插入残影小球。
将原有实现包装在一个函数里
let animat = (初始位置, 结束位置) => {
...参数设定
// 位置变换
nowX += Xspeed;
nowY += (A * loop + Yspeed);
requestAnimationFrame(() => {
Node.style.top = nowY + 'px';
Node.style.left = nowX + 'px';
loop++;
move();
});
}
复制代码
目的很简单,就是生成的残影的小球也须要和原有小球位置信息同步。
思考:每一次残影小球的位置都要与真实小球相关。(经过相同初始值设定的小球天然轨迹相同) 因此咱们不能变更小球的真实位置,那么translate彷佛就是一个不错的选择。
let animat = (初始位置, 结束位置, 是不是残影) => {
...参数设定
// 位置变换
nowX += Xspeed;
nowY += (A * loop + Yspeed);
requestAnimationFrame(() => {
Node.style.top = nowY + 'px';
Node.style.left = nowX + 'px';
if (isShadow) {
item.style.transform = `translate(${(0.5 * Xspeed)}px ,${-(0.5 * (A * loop + Yspeed))}px)`;
item.style.opacity = 0.5;
}
}
loop++;
move();
});
}
复制代码
这一步须要注意的是 透明度的变化相当重要。 透明度的取值笔者推荐 0.1至 0.5之间。
若是只是一个生成一个小球的话,动态模糊的效果不会和明显。因此咱们须要新建一个控制小球数量的函数。
createShadow(初始位置, 结束位置, num) {
for (let i = 0; i < num; i++) {
animat(初始位置, 结束位置, true, i / (num + 1));
}
},
复制代码
animat函数更改成
let animat = (初始位置, 结束位置, 是不是残影, num) => {
.....
requestAnimationFrame(() => {
....
if (isShadow) {
item.style.transform = `translate(${-(num * Xspeed)}px ,${-(num * (A * loop + Yspeed))}px)`;
item.style.opacity = (1 - num) * 0.5;
}
}
.....
});
}
复制代码
大功告成。
若是对本文有不解,不赞同之处或你有更好的点子,请在留言区留言。一块儿交流,共同进步。