需求
- 页面中的盒子可以拖拽,并且在松开鼠标左键后可以做抛物线运动弹性势能动画。如果到达右边界,则会反弹回来,到达左边界,也会反弹回来,最终停留在某一位置。
- 如果用户拖拽时移动的速度快,那么要求盒子在水平方向上的初始速度大,反之则小;
分析
- 拖拽功能前面已经实现;
- 抛物线运动可以分解成水平方向的运动和垂直方向的运动。
- 水平方向运动:
- 出手后,盒子会有匀减速动画,想要实现匀减速,只需要给盒子的原有速度不断乘以一个小于1的数字,例如0.98,即可实现速度的指数衰减;
- 用户拖动时速度快,则运动的快,出手速度慢则出手速度慢。这有个问题,我们怎么计算速度?什么时候计算?
- 速度是单位时间内物体运动的距离。而出手之前盒子一直处于拖拽状态,所以速度也该拖拽的过程中计算。这里有一点值得注意,浏览器两次 mousemove 事件触发之间的时间时固定的。因此我们可以把两次 mousemove 之间的时间作为单位时间,接下来如果我们可以计算出在两次 mousemove 时间内鼠标走过的距离,就可以作为出手速度了。
还有一个问题,只要盒子移动就会触发 mousemove 事件,那么我们怎么知道取哪一次的计算结果呢?首先我们在开始拖动之初就使用一个属性 prevX 记录下鼠标的初始位置,然后我们移动过程中,把本次鼠标的位置e.pageX 减去上一次 mousemove 触发时鼠标所在的位置 prevX,就是这一次移动相对于上一次移动移动的距离,即本次移动的速度 hSpeed;接下来我们把本次鼠标停留的位置赋值给 prevX,依次类推,prevX存储的就是上一次 mousemove 触发时鼠标停留的位置,直到松开鼠标左键时,hSpeed 存储的就是上一次触发 mousemove 事件时移动距离,即上一次的移动速度。
越界判断:
右边界:即浏览器可视窗口的右边;盒子在这个距离内可以运动的最大距离是,即盒子的 left 的最大值是:浏览器可视窗口宽度 - 盒子的 offsetWidth 左边界:即浏览器的可视窗口的左边;即 left 为0的
- 何时开始动画?拖拽结束,即鼠标左键抬起时触发。
- 如何实现触界反弹?给速度乘以-1即可;
- 何时停止动画呢?因为用初速度乘以0.98,这个值永远不为0,但是当这个值小于1时,盒子的改变小于1px,肉眼基本不可见,此时我们就可以选择清除动画。
- 垂直方向运动:
- 垂直方向上是变速运动,按照常理来讲,物体自由从空中下落速度是会越来越快的,所以我们的下落速度dropSpeed应该是一个越来越大的值。真实下落的物体的速度肯定是每时每刻都在变大,但是我们的动画只能是每隔一定毫秒数执行一次,所以我们每次运动时都给垂直速度加上一个值,以保证这次动画的速度大于上一次。
- 同时因为物体在下落的过程中,由于空气阻力等因素,其速度也会衰减一部分,所以我们在下落的过程总还要给速度乘以一个小于1的数字,以模拟动能的损失。
- 物体在垂直方向上可以运动的最大距离同样是有限的,即最小的是0,最大也是 浏览器窗口高度 - 盒子的 offsetHeight。同样触界后也需要反弹,反弹的实现原理同样是给速度乘以 -1即可。
- 何时停止动画?垂直方向的动画和水平方向的不同,垂直方向上是当这个物体弹不起来的时候就该不该再进行动画了。所以我们需要一个标识符 n,每次当他弹起来,我们就将这个标识符置为0,每次触底一次,就给这个标识符++,如果这一次触底的话,n 就变成了1,如果弹起来 n 就改为0,直到盒子不能弹起来时,n 成为了1,下一次在执行动画时,盒子仍然没能弹起来,就继续执行,此时 n 已经是2了。如果是这种情况说明这个盒子弹不起来了,此时应该停止动画。
let box = document.getElementById('box');
box.onmousedown = dragStart;
function dragStart(e) {
// 1. 记录鼠标初始位置
this.startX = e.pageX;
this.startY = e.pageY;
// 2. 记录盒子初始left和top值
this.startL = parseFloat(this.style.left);
this.startT = parseFloat(this.style.top);
// 3. 绑定点击事件
this.DRAGM = dragMove.bind(this);
this.DRAGE = dragEnd.bind(this);
document.addEventListener('mousemove', this.DRAGM, false);
document.addEventListener('mouseup', this.DRAGE, false);
}
function dragMove(e) {
// 1. 计算当前盒子应该处于的位置
let curL = e.pageX - this.startX + this.startL;
let curT = e.pageY - this.startY + this.startT;
// 2. 将元素设置到鼠标的位置
this.style.left = `${curL}px`;
this.style.top = `${curT}px`;
// 4. 判断初始点击时记录初始位置,因为我们需要计算两次鼠标移动事件触发之间鼠标走过的距离,所以需要记录这个初始位置。
if (!this.prevX) this.prevX = this.startX;
this.hSpeed = e.pageX - this.prevX;
this.prevX = e.pageX;
// 5. 设置自由落体初速度
this.dropSpeed = 5;
}
function dragEnd(e) {
// 1. 移除事件
document.removeEventListener('mousemove', this.DRAGM, false);
document.removeEventListener('mouseup', this.DRAGE, false);
// 2. 就算盒子的边界
this.maxL = (document.documentElement.clientWidth || document.body.clientWidth) - this.offsetWidth;
this.maxT = (document.documentElement.clientHeight || document.body.clientHeight) - this.offsetHeight;
this.flyTimer = setInterval(() => fly.call(this), 20);
this.dropTimer = setInterval(() => drop.call(this), 20);
}
function fly() {
// 1. 计算速度
this.hSpeed *= .98;
// 2. 计算此时盒子应该处于的位置
let l = parseFloat(this.style.left) + this.hSpeed;
// 3. 过界判断,如果过界则修正,同时速度改变方向
if (l > this.maxL) {
// 盒子越过右边界了
l = this.maxL;
this.hSpeed *= -1;
}
if (l < 0) {
// 盒子越过左边界
l = 0;
this.hSpeed *= -1
}
// 4. 将修正后的位置设置给盒子
this.style.left = `${l}px`;
// 5. 当速度小到一定程度时,盒子的位置改变量就很小了,所以此时就没必要在继续动画了,因为速度有正负,
// 所以我们在判断当速度小于某个值的时候清除定时器,同时终止动画
if (Math.abs(this.hSpeed) < 1) {
clearInterval(this.flyTimer);
}
}
function drop() {
if (!this.n) this.n = 0;
this.dropSpeed += 5;
this.dropSpeed *= .98;
let t = parseFloat(this.style.top) + this.dropSpeed;
if (t > this.maxT) {
t = this.maxT;
this.dropSpeed *= -1;
this.n++;
} else {
this.n = 0
}
this.style.top = `${t}px`;
if (this.n > 2) clearInterval(this.dropTimer);
}