微信小程序使用贝塞尔曲线绘制加入购物车动画

定义

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。

用途

以上介绍摘自百度百科,看起来有点晦涩难懂,一年多以前我第一次接触到这个名词便心生抗拒之意。然而在前端开发中,我们在实现抛物线动画时,贝塞尔曲线绝对是个好帮手。今天我们通过在微信小程序实现一个常见的加入购物车动画,来进一步理解它。

理解


通过上图,我们可以看出贝塞尔曲线有四个关键点,P0、P1、P2、P3且x、y轴取值范围在 [0,1] 。MDN上特别说明了:

Cubic Bézier curves with the P1 or P2 ordinate outside the [0, 1] range may generate bouncing effects.
When you specify an invalid cubic-bezier curve, CSS ignores the whole property.

所以x、y的取值一旦不在区间内,CSS会 忽略 掉。
我们只需要关注P1和P2的取值,就可以生成一条曲线。在CSS中,贝塞尔曲线的表达式参数也是这两个坐标:

1
cubic-bezier(x1, y1, x2, y2)

贝塞尔曲线属于 缓动函数(easing functions) ,用于表现某段时间内物体 运动的快慢 。我们可以通过一些实用的工具来调整出我们需要的参数
https://easings.net/
有多种配置好的曲线,应付常见需求应该没有问题。
https://cubic-bezier.com/#.17,.67,.83,.67
贝塞尔曲线在线编辑器,可以在线预览效果。url后面的数字即对应四个参数。

实践

铺垫了那么多,我们来看一下怎么实现购物车动画。网上有许多该动画在原生js和Vue的实现,其实道理都是一样的:我们需要先获取到「加入购物车」按钮、「购物篮」的坐标,然后通过贝塞尔曲线改变小球的 top 值和 left 值实现从起点到终点的平滑过渡。
dom结构:

1
2
3
4
5
6
7
// 触发按钮及小球
<view class="btn-add-cart before-position" bindtap="addCart">加入购物车动画</view>
<view wx:if="{{ballDisplay}}" class="ball" style="top:{{top}};left:{{left}}"></view>
// 底部tabbar购物栏
<view class="bottom-bar" hover-class="none" hover-stop-propagation="false">
<image id="icon" class="icon after-position" src="../../images/cart/cart.png" mode="widthFix"></image>
</view>

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

.btn-add-cart {
width: 300rpx;
height: 50rpx;
position: relative;
}

.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
height: 100rpx;
width: 100%;
background-color: #333;
display: flex;
align-items: center;
justify-content: flex-end
}
.icon{
height:64rpx;
width:64rpx;
padding:10rpx;
margin-right:30rpx;
border-radius: 50%;
background-color: royalblue;
}
.ball{
position: absolute;
width:30rpx;
height:30rpx;
background-color: red;
border-radius: 50%;
z-index:999;
transition: 1s top cubic-bezier(0.47, 0, 0.745, 0.715), 1s left cubic-bezier(0, 0, 0, 0);
}

然后写个通用方法获取起点和终点的坐标:

1
2
3
4
5
6
7
8
9
10
11
12
getRects(cls) {
return new Promise((resolve, reject) => {
wx.createSelectorQuery()
.in(this)
.select(cls)
.boundingClientRect(function(rect) {
console.log(rect);
resolve(rect);
})
.exec();
});
}

注意:获取小球初始位置要在 onShow() 获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
onShow() {
// 获取小球最开始的位置
this.initBallPos();
this.start = false;
},
initBallPos() {
this.getRects(".before-position").then(rect => {
this.setData({
top: `${rect.top + 5}px`,
left: `${rect.left + 30}px`,
originTop: `${rect.top + 5}px`,
originLeft: `${rect.left + 30}px`
});
});
}

这里是触发函数,用 this.start 防止多次触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 加入购物车动画
addCart() {
let that = this;
// 禁止动画多次触发
if (this.start) {
return;
}
this.start = true;
this.setData({
ballDisplay: true
});
// 获取小球终点位置
this.getRects(".after-position").then(rect => {
this.setData({
top: `${rect.top + 16}px`,
left: `${rect.left + 16}px`
});
// 延时跟动画时长一致,飞完隐藏掉,再把小球重置到初始位置。
let { originLeft, originTop } = this.data;
setTimeout(() => {
that.setData({
ballDisplay: false,
top: originTop,
left: originLeft
});
that.start = false;
}, 1000);
});
}

demo效果:

具体代码可在Github找到哦~