原生JS实现抛物线动画以及动态模糊效果

avatar
SugarTurboS Club @SugarTurboS
  • 苏格团队
  • 作者:Jason

前言

某一天我收到了产品发来的微信消息。小X,我们的业务现在需要一个类似加入购物车的掉落动画,经过组织的慎重考虑,这个需求就交给你了。于是便有了这篇文章。本文并没有描述多少高深的技术,更多的是一些笔者在做动画时对动画原理的思考以及如何优化动画的一些思路。实现效果如下:

图片质量有限,请谅解

技术分析

前端实现动画的方式有很多。无论是JS动画,CSS动画,Canvas动画还是SVG动画,哪怕是GIF动画实现一个简单的抛物线都是足够的。但考虑业务场景的需求以及可玩性,最终决定使用JS来实现这个动画。

实现分析

在笔者看来大多数动画效果,归根到底还是 数学公式的应用。所谓抛物线动画也无非就是让元素的运动符合抛物线的运动轨迹。
抛物线的方程为: Y = A*X*X + B*X + C 也许大家看到这个公式有点陌生。但曾经物理老师念念有词的 L(距离) = 1/2*A(加速度)*T*T(时间) 想必大家都一定熟记于心。笔者正是利用这个公式来完成抛物线动画。

具体实现

步骤一 获取抛物线的起点和终点

由于业务本身的特殊性,需要在用户点击物品时获取到该元素在窗口中的绝对位置。即元素相对于浏览器可见区域的X, Y的坐标。 这里笔者推荐使用getBoundingClientRect()函数结合具体业务计算绝对位置。 当然,在一些场景里你可以直接使用鼠标点击位置或者其他任意方法获取动画的起点。

步骤二 设定抛物线参数

1. 加速度

加速度A决定了元素在设定方向(下文都用垂直方向代替)的速度变化快慢。当动画的起点和终点都固定时,由公式 L(垂直距离) = 0.5 * A(加速度) * T * T(时间) 可得出此时 加速度A与时间T的平方成反比
需要注意的是,正的加速度A会一直扩大帧与帧之间的垂直移动距离,所以过大的加速度A可能会导致 动画的末期小球有闪烁感

2. 时间 T 与 X 轴初速度

在抛物线的动画中,一般的我们认为元素的 水平移动速度固定。那么同样由公式 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); 

如果需要动画执行的时长固定呢?

// 确定关键参数
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');
// 自由控制形体,定位一般设定为Fixed
Node.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(() => {
        Node.style.top = nowY + 'px';
        Node.style.left = nowX + 'px';
        loop++;
        move();
    });
};

3. 小球可能会超过目的点

根据停止动画的代码逻辑,小球在最后一次位移时,也许会超越我们设定的目的点。在下一次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;   
        }
    }
    .....
    });
}

大功告成。

结束语

如果对本文有不解,不赞同之处或你有更好的点子,请在留言区留言。一起交流,共同进步。