京东购物车动效实现:贝塞尔曲线的妙用

10,147 阅读3分钟

前言

大家好,我是奈德丽。前两天在逛京东想买Pocket 3的时候,注意到了它的购物车动效,当点击"加入购物车"按钮时,一个小红球从商品飞入购物车,我觉得很有意思,于是花了点时间来研究。

实现效果

看了图才知道我在讲什么,那么先看Gif吧!

JDmall-1.gif

代码演示

代码已经上传到了码上掘金,感兴趣的可以自行查看,文章中没有贴全部代码了,主要讲讲思路, code.juejin.cn/pen/7503150…

实现思路

下面这个思路,小白也能会,我们将通过以下几个步骤来实现这个效果:

画页面——>写逻辑实现动画效果

好了,废话不多说,开始进入正题

第一步:先让AI帮我们写出来UI结构

像我们这种工作1坤年以上的切图仔,能偷懒当然偷懒啦,这种画页面的活可以丢给AI来干了,下面是Taro帮我生成的页面部分,没什么难点,就是一些普普通通的页面元素。

<template>
  <div class="rolling-ball-container">
    <!-- 商品列表 -->
    <div class="item-list">
      <div class="item" v-for="item in 10" :key="item">
        <div class="product-card">
          <div class="product-tag">秒杀</div>
          <div class="product-image">
            <img src="/product.jpg" alt="商品图片" />
          </div>
          <div class="product-info">
            <div class="product-title">大疆 DJI Osmo Pocket 3 一英寸口袋云台相机</div>
            <div class="product-features">
              <span class="feature-tag">三轴防抖</span>
              <span class="feature-tag">防抖稳定</span>
              <span class="feature-tag">高清画质</span>
            </div>
            <div class="product-price">
              <span class="price-symbol">¥</span>
              <span class="price-value">4788</span>
              <span class="price-original">¥4899</span>
            </div>
            <div class="product-meta">
              <span class="delivery-time">24分钟达</span>
              <span class="rating">好评率96%</span>
            </div>
            <div class="product-shop">京东之家-凯德汇新店</div>
          </div>
          <div class="add-to-cart" @click="startRolling($event)">+</div>
        </div>
      </div>
    </div>
    
    <!-- 购物车图标 -->
    <div class="point end-point">
      <div style="position: relative;">
        <img src="/cart.png" />
        <div class="cart-count">{{ totalCount }}</div>
      </div>
    </div>
    
    <!-- 小球容器 -->
    <div 
      v-for="(ball, index) in balls" 
      :key="index"
      class="ball"
      v-show="ball.show"
      :style="getBallStyle(ball)"
    ></div>
  </div>
</template>




第二步:设计小球数据模型

有了页面元素了,我们需要创建小球数组和计数器

import { reactive, ref } from 'vue';

// 购物车商品计数
const totalCount = ref(0);

// 创建小球数组(预先创建3个小球以应对连续点击)
const balls = reactive(Array(3).fill(0).map(() => ({
  show: false,   // 是否显示
  startX: 0,     // 起点X坐标
  startY: 0,     // 起点Y坐标
  endX: 0,       // 终点X坐标
  endY: 0,       // 终点Y坐标
  pathX: 0,      // 路径X偏移量
  pathY: 0,      // 路径Y偏移量
  progress: 0    // 动画进度
})));

为什么小球要用一个数组来存储呢?因为我看到京东上用户是可以连续点击+号将商品加入购入车的,页面上可以同时存在很多个飞行的小球。

第三步:实现动画触发函数

当用户点击"+"按钮时,我们需要计算起点和终点坐标,然后启动动画,这儿有一个细节,为了让小球刚好落到在购物车中间,对终点坐标进行了微调。

// 开始滚动动画
const startRolling = (event: MouseEvent) => {
  // 获取起点和终点元素
  const startPoint = event.currentTarget as HTMLElement;
  const endPoint = document.querySelector('.end-point') as HTMLElement;
  
  if (startPoint && endPoint) {
    // 找到一个可用的小球
    const ball = balls.find(ball => !ball.show);
    if (ball) {
      // 获取起点位置
      const startRect = startPoint.getBoundingClientRect();
      ball.startX = startRect.left + startRect.width / 2;
      ball.startY = startRect.top + startRect.height / 2;
      
      // 获取终点位置
      const endRect = endPoint.getBoundingClientRect();    
      const endX = endRect.left + endRect.width / 2;
      const endY = endRect.top + endRect.height / 2;
      // 微调终点位置
      ball.endX = endX - 4;
      ball.endY = endY - 7;
      
      // 设置路径偏移量
      ball.pathX = 0;
      ball.pathY = 100;
      
      // 显示小球并重置进度
      ball.show = true;
      ball.progress = 0;
      
      // 使用requestAnimationFrame实现动画
      let startTime = Date.now();
      const duration = 400; // 动画持续时间(毫秒)
      
      function animate() {
        const currentTime = Date.now();
        const elapsed = currentTime - startTime;
        ball.progress = Math.min(elapsed / duration, 1);
        
        if (ball.progress < 1) {
          requestAnimationFrame(animate);
        } else {
          // 动画结束后隐藏小球
          setTimeout(() => {
            ball.show = false;
          }, 100);
        }
      }
      
      requestAnimationFrame(animate);
      
      // 增加购物车商品数量
      totalCount.value++;
    }
  }
};

第四步:使用贝塞尔曲线计算小球轨迹

点击"+"按钮,不能让小球做自由落体运动吧,那是伽利略研究的,你看这自由落体好看嘛,指定不行,要是长这样,那东哥的商城还能卖出去东西吗?Hah

JDmall-2.gif

为了不让它自由落体,给它一个向左的偏移量100px

// 获取小球样式
const getBallStyle = (ball: any) => {
  if (!ball.show) return {};
  
  // 使用二次贝塞尔曲线计算路径
  const t = ball.progress;
  const mt = 1 - t;
  
  // 判断起点和终点是否在同一垂直线上
  const isVertical = Math.abs(ball.startX - ball.endX) < 20;
  
  // 计算控制点(确保有弧度)
  let controlX, controlY;
  
  if (isVertical) {
    // 如果在同一垂直线上,向左偏移一定距离
    controlX = ball.startX - 100;
    controlY = (ball.startY + ball.endY) / 2;
  } else {
    // 否则使用向左偏移
    controlX = (ball.startX + ball.endX) / 2 - 100;
    controlY = (ball.startY + ball.endY) / 2 + (ball.pathY || 100);
  }
  
  // 二次贝塞尔曲线公式
  const x = mt * mt * ball.startX + 2 * mt * t * controlX + t * t * ball.endX;
  const y = mt * mt * ball.startY + 2 * mt * t * controlY + t * t * ball.endY;
  
  return {
    left: `${x}px`,
    top: `${y}px`,
    transform: `rotate(${ball.progress * 360}deg)` // 添加旋转效果
  };
};

技术要点解析

1. 贝塞尔曲线原理

贝塞尔曲线是一种参数化曲线,广泛应用于计算机图形学。二次贝塞尔曲线由三个点定义:起点P₀、控制点P₁和终点P₂。

曲线上任意点的坐标可以通过以下公式计算:

B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂  (0 ≤ t ≤ 1)

在我们的实现中,通过调整控制点的位置,可以控制曲线的形状,从而实现小球的抛物线运动效果。

2. requestAnimationFrame的优势

与setTimeout或setInterval相比,requestAnimationFrame有以下优势:

  1. 性能更好:浏览器会在最合适的时间(通常是下一次重绘之前)执行回调函数,避免不必要的重绘
  2. 节能:当页面不可见或最小化时,动画会自动暂停,节省CPU资源
  3. 更流畅:与显示器刷新率同步,动画更平滑

3. 动态计算元素位置

我们使用getBoundingClientRect()方法获取元素在视口中的精确位置,这确保了无论页面如何滚动或调整大小,动画始终能准确地从起点到达终点。

总结

通过这个小球飞入购物车的动画效果,我们不仅提升了用户体验,还学习了:

  1. 如何使用贝塞尔曲线创建平滑动画
  2. 如何用requestAnimationFrame实现高性能动画
  3. 如何动态计算元素位置
  4. 如何使用rem单位实现移动端适配

这个小小的交互设计虽然看起来简单,但能大大提升用户体验,让你的电商网站更加生动有趣。从京东商城的灵感到实际代码实现,我们完成了一个专业级别的交互效果。

恩恩……懦夫的味道