起因
我们产品要我仿照链家写一个收藏轨迹动画!
初步实现
初始思路,使用css去做一个处理探秘神奇的运动路径动画 Motion Path
其中有一个这个:
那我想,这样我不就可以把弧度稍微往下一点,中间点和终点往上边挪一下,不就成功了么!
于是得到
第一版本:
<!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>
pass原因:初步看是没有任何问题的,但由于手机分辨率的问题,轨迹运动点不是固定的,没有很好的办法找到轨迹的转折点。
亿点细节
在和产品讨论,和重新看了一下其他app的轨迹路线之后,发现动画轨迹的运动路线和我们高中学习的抛物线方程式有些相似,只不过一个是上开口,一个是下开口,按照这个思路,写出了第二版
第一步 获取起始点和终端,计算前半段和后半段偏移量
// 获取起始位置
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
贝塞尔实现
<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);
得到结果