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