拖拽+弹性动画

需求:

  • 页面中的盒子可以拖拽,并且在松开鼠标左键后可以做抛物线运动弹性势能动画。如果到达右边界,则会反弹回来,到达左边界,也会反弹回来,最终停留在某一位置。
  • 如果用户拖拽时移动的速度快,那么要求盒子在水平方向上的初始速度大,反之则小;

分析:

  • 拖拽功能前面已经实现;
  • 抛物线运动可以分解成水平方向的运动和垂直方向的运动。
  • 水平方向运动:
  • 出手后,盒子会有匀减速动画,想要实现匀减速,只需要给盒子的原有速度不断乘以一个小于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了。如果是这种情况说明这个盒子弹不起来了,此时应该停止动画。
  1. let box = document.getElementById('box');
  2. box.onmousedown = dragStart;
  3. function dragStart(e) {
  4. // 1. 记录鼠标初始位置
  5. this.startX = e.pageX;
  6. this.startY = e.pageY;
  7. // 2. 记录盒子初始left和top值
  8. this.startL = parseFloat(this.style.left);
  9. this.startT = parseFloat(this.style.top);
  10. // 3. 绑定点击事件
  11. this.DRAGM = dragMove.bind(this);
  12. this.DRAGE = dragEnd.bind(this);
  13. document.addEventListener('mousemove', this.DRAGM, false);
  14. document.addEventListener('mouseup', this.DRAGE, false);
  15. }
  16. function dragMove(e) {
  17. // 1. 计算当前盒子应该处于的位置
  18. let curL = e.pageX - this.startX + this.startL;
  19. let curT = e.pageY - this.startY + this.startT;
  20. // 2. 将元素设置到鼠标的位置
  21. this.style.left = `${curL}px`;
  22. this.style.top = `${curT}px`;
  23. // 4. 判断初始点击时记录初始位置,因为我们需要计算两次鼠标移动事件触发之间鼠标走过的距离,所以需要记录这个初始位置。
  24. if (!this.prevX) this.prevX = this.startX;
  25. this.hSpeed = e.pageX - this.prevX;
  26. this.prevX = e.pageX;
  27. // 5. 设置自由落体初速度
  28. this.dropSpeed = 5;
  29. }
  30. function dragEnd(e) {
  31. // 1. 移除事件
  32. document.removeEventListener('mousemove', this.DRAGM, false);
  33. document.removeEventListener('mouseup', this.DRAGE, false);
  34. // 2. 就算盒子的边界
  35. this.maxL = (document.documentElement.clientWidth || document.body.clientWidth) - this.offsetWidth;
  36. this.maxT = (document.documentElement.clientHeight || document.body.clientHeight) - this.offsetHeight;
  37. this.flyTimer = setInterval(() => fly.call(this), 20);
  38. this.dropTimer = setInterval(() => drop.call(this), 20);
  39. }
  40. function fly() {
  41. // 1. 计算速度
  42. this.hSpeed *= .98;
  43. // 2. 计算此时盒子应该处于的位置
  44. let l = parseFloat(this.style.left) + this.hSpeed;
  45. // 3. 过界判断,如果过界则修正,同时速度改变方向
  46. if (l > this.maxL) {
  47. // 盒子越过右边界了
  48. l = this.maxL;
  49. this.hSpeed *= -1;
  50. }
  51. if (l < 0) {
  52. // 盒子越过左边界
  53. l = 0;
  54. this.hSpeed *= -1
  55. }
  56. // 4. 将修正后的位置设置给盒子
  57. this.style.left = `${l}px`;
  58. // 5. 当速度小到一定程度时,盒子的位置改变量就很小了,所以此时就没必要在继续动画了,因为速度有正负,所以我们在判断当速度小于某个值的时候清除定时器,同时终止动画
  59. if (Math.abs(this.hSpeed) < 1) {
  60. clearInterval(this.flyTimer);
  61. }
  62. }
  63. function drop() {
  64. if (!this.n) this.n = 0;
  65. this.dropSpeed += 5;
  66. this.dropSpeed *= .98;
  67. let t = parseFloat(this.style.top) + this.dropSpeed;
  68. if (t > this.maxT) {
  69. t = this.maxT;
  70. this.dropSpeed *= -1;
  71. this.n++;
  72. } else {
  73. this.n = 0
  74. }
  75. this.style.top = `${t}px`;
  76. if (this.n > 2) clearInterval(this.dropTimer);
  77. }