轨迹动画小记

794 阅读3分钟

起因

我们产品要我仿照链家写一个收藏轨迹动画!

初步实现

初始思路,使用css去做一个处理探秘神奇的运动路径动画 Motion Path 其中有一个这个: "图片自定义高度" height="" width="" 那我想,这样我不就可以把弧度稍微往下一点,中间点和终点往上边挪一下,不就成功了么! 于是得到第一版本:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .ball {
            position: aboslute;
            bottom: 0;
            width: 40px;
            height: 40px;
            offset-path: path('M10 844 C 80 350, 140 350, 390 0 S 390 0, 390 0');
            offset-anchor: 0 100%;
            animation: move 3000ms infinite alternate linear;
        }
        @keyframes move {
            0% {
                offset-distance: 0%;
            }

            100% {
                offset-distance: 100%;
            }
        }
    </style>
</head>
<body>
    <img src="https://assets.che300.com/feimg/h5/c-353/car_icon_shoucang_s@3x.png" class="ball" alt="" srcset="">
</body>
</html>

"图片自定义高度" height="200" width="" pass原因:初步看是没有任何问题的,但由于手机分辨率的问题,轨迹运动点不是固定的,没有很好的办法找到轨迹的转折点。

亿点细节

在和产品讨论,和重新看了一下其他app的轨迹路线之后,发现动画轨迹的运动路线和我们高中学习的抛物线方程式有些相似,只不过一个是上开口,一个是下开口,按照这个思路,写出了第二版 "图片自定义高度" height="" width=""

第一步 获取起始点和终端,计算前半段和后半段偏移量

 // 获取起始位置
let startX = Math.floor(startWink.getBoundingClientRect().left);
let startY = Math.floor(startWink.getBoundingClientRect().top);

// 获取结束位置
let endX = Math.floor(collectionFile.getBoundingClientRect().left);
let endY = Math.floor(collectionFile.getBoundingClientRect().top);

// 获取中间位置
let harfX = (body.clientWidth * 2) / 5;
let harfY = (body.clientHeight * 1) / 2;

// 前半段偏移量
let firstX = harfX - startX;
let firstY = harfY - startY;

let moveFirstX = 0;
let moveFirstY = 0;

// 后半段偏移
let lastX = endX - harfX;
let lastY = endY - harfY;

第二步 根据方程式 y = a * x * x + bx 计算下开口方程式的左半边

let a = 0.009;
let b = (firstY - a * firstX * firstX) / firstX;
let timer = null;
let duration = 200;

x = Math.floor(firstX * ((now - beginTime) / duration));
y = Math.floor(a * x * x + b * x);

moveFirstX = startX + x;
moveFirstY = startY + y;
moveStar.style.cssText = `position:fixed;left:${startX + x}px;top:${startY + y}px;`;

第三步根据方程式 y = -a * x * x + bx 计算上开口方程式右半边

let a2 = -0.012;
let timer2 = null;
let duration2 = 500;

// 计算每一步的X轴的位置
moveStar.className = `moveStar`;
x = Math.floor(lastX * ((now - beginTime2) / duration2));
y = Math.floor(a2 * x * x + b2 * x);

moveStar.style.cssText = `position:fixed; left:${moveFirstX + x}px;top:${moveFirstY + y}px;`;

第二步和第三步通过定时器setInterval进行清空;

新发现

贝塞尔曲线

贝塞尔曲线,是应用于二维图形应用程序的数学曲线,由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋。

贝塞尔曲线的类型有很多,以我们今天要讲的为例子,三次贝塞尔曲线。在此之外还有一次贝塞尔和二次贝塞尔曲线;

以我们上面的轨迹为例子:

三次贝塞尔曲线需要定义一个点两个控制点,需要设置三组坐标参数

path('M10 844 C 80 350, 140 350, 390 0)
              C x1 y1, x2 y2, x y
  • (x,y)曲线的终点
  • (x1,y1)起点的控制点
  • (x2,y2)终点的控制点

我们可以将若干个贝塞尔曲线连起来,从而创建出一条很长的平滑曲线。通常情况下,一个点某一侧的控制点是它另一侧的控制点的对称以保持斜率不变

path('M10 844 C 80 350, 140 350, 390 0 S 390 0, 390 0')
               C x1 y1, x2 y2, x y     S x2 y2, x y

"图片自定义高度" height="" width=""

贝塞尔实现

<div style="position:absolute;left:0;top:0;width:500px;height:300px;overflow:hidden;">
</div>
<img src="https://assets.che300.com/feimg/h5/c-353/car_icon_shoucang_s@3x.png" id="dotMove"
    style="position:absolute;width:40px;height:40px;overflow:hidden;">

    /*
     cp[0]起点
     cp[1]第一个控制点
     cp[2]第二个控制点
     cp[3]结束点
     t参数值,0 <= t <= 1
    */
    function Point2D(x, y) {
        this.x = x || 0.0;
        this.y = y || 0.0;
    }
    function PointOnCubicBezier(cp, t) {
        var ax, bx, cx;
        var ay, by, cy;
        var tSquared, tCubed;
        var result = new Point2D;
        /*计算*/
        cx = 3.0 * (cp[1].x - cp[0].x);
        bx = 3.0 * (cp[2].x - cp[1].x) - cx;
        ax = cp[3].x - cp[0].x - cx - bx;
        cy = 3.0 * (cp[1].y - cp[0].y);
        by = 3.0 * (cp[2].y - cp[1].y) - cy;
        ay = cp[3].y - cp[0].y - cy - by;
        /*计算参数t的曲线点*/
        tSquared = t * t;
        tCubed = tSquared * t;
        result.x = (ax * tCubed) + (bx * tSquared) + (cx * t) + cp[0].x;
        result.y = (ay * tCubed) + (by * tSquared) + (cy * t) + cp[0].y;
        return result;
    }

    function ComputeBezier(cp, numberOfPoints, curve) {
        var dt;
        var i;
        dt = 1.0 / (numberOfPoints - 1);
        for (i = 0; i < numberOfPoints; i++)
            curve[i] = PointOnCubicBezier(cp, i * dt);
    }
    var cp = [
        new Point2D(0, 844), new Point2D(0, 400), new Point2D(390, 400), new Point2D(390, 0)
    ];
    var numberOfPoints = 100;
    var curve = [];
    ComputeBezier(cp, numberOfPoints, curve);
    var i = 0, dot = document.getElementById("dotMove");
    setInterval(function () {
        var j = (i < 100) ? i : (199 - i);
        dot.style.left = curve[j].x + 'px';
        dot.style.top = curve[j].y + 'px';
        if (++i == 200) i = 0;
    }, 50);

"图片自定义高度" height="" width=""

得到结果 "图片自定义高度" height="" width=""

cubic-bezier.com/