美团购物车小球动画效果

前言

最近做了一个小需求,记得之前写过这个美团购物车小球动画效果还发在csdn里面了,但需要的时候却找不到,不知道什么时候给删了,今天再给大家分享一下这个功能的源码,以便以后的使用。

先来看看效果

具体实现逻辑

每次点击时,拿到点击的位置作为小球的开始位置,再获取到购物车的结束位置。确定了两端位置之后,给小球设置css的path路径(使用贝塞尔曲线),最后通过animate方法执行动画效果,即可实现。

核心代码

javascript 复制代码
// 小球的css设置
.ball {
  width: 20px;
  height: 20px;
  background: linear-gradient(135deg, #ff3000, #ff9000);
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  position: absolute;
  top: 0;
  right: 0;
}

// 添加购物车方法
const addToCart = (item, event) => {
   cart.count++;
   cart.total += item.price;

   // 获取点击位置
   const startX = event.clientX;
   const startY = event.clientY;
   createBall(startX, startY);
 };

 // 创建小球
 const createBall = (startX, startY) => {
   let endEle = document
     .querySelector(".cart")
     .getBoundingClientRect();
   let endX = Math.floor(endEle.left + endEle.width / 2);
   let endY = Math.floor(endEle.top + endEle.height / 2);
   let fatherEle = document.querySelector(".container");
   let ball = document.createElement("div");
   ball.classList.add("ball");
   ball.style.left = startX + "px";
   ball.style.top = startY + "px";
   // 贝塞尔曲线路径
   ball.style.offsetPath = `path('M${0} ${0} C${100} ${-100}, ${
     endX - startX
   } ${endY - startY}, ${endX - startX} ${endY - startY}')`;

   fatherEle.appendChild(ball);
   setTimeout(() => {
     fatherEle.removeChild(ball);
   }, Number(animationSpeed.value) - 100);
   ball.animate(
     // 将偏移路径动画化
     { offsetDistance: [0, "100%"] },
     {
       duration: Number(animationSpeed.value),
       iterations: 1,
       easing: "cubic-bezier(.667,0.01,.333,.99)",
       direction: "alternate",
     }
   );
 };

源码分享

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>美团购物车小球动画</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        font-family: "PingFang SC", "Helvetica Neue", Arial, sans-serif;
        background-color: #f5f5f5;
        padding: 20px;
        color: #333;
      }
      .container {
        max-width: 800px;
        margin: 0 auto;
        background: white;
        border-radius: 12px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        padding: 20px;
      }
      h1 {
        text-align: center;
        color: #ffd000;
        margin-bottom: 20px;
      }
      .description {
        text-align: center;
        margin-bottom: 30px;
        color: #666;
      }
      .items-container {
        display: flex;
        flex-wrap: wrap;
        gap: 15px;
        justify-content: center;
        margin-bottom: 30px;
      }
      .item {
        width: 100px;
        padding: 10px;
        background: #f8f8f8;
        border-radius: 8px;
        text-align: center;
        cursor: pointer;
        transition: transform 0.2s;
      }
      .item:hover {
        transform: translateY(-3px);
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      }
      .item-img {
        width: 60px;
        height: 60px;
        background: linear-gradient(135deg, #ffd000, #ffb800);
        border-radius: 50%;
        margin: 0 auto 8px;
      }
      .cart-container {
        position: fixed;
        bottom: 20px;
        right: 20px;
      }
      .cart {
        width: 60px;
        height: 60px;
        background: #ffd000;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      }
      .cart-count {
        position: absolute;
        top: -5px;
        right: -5px;
        background: #ff3000;
        color: white;
        font-size: 12px;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .ball-container {
        position: absolute;
        pointer-events: none;
      }
      .ball {
        width: 20px;
        height: 20px;
        background: linear-gradient(135deg, #ff3000, #ff9000);
        border-radius: 50%;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        position: absolute;
        top: 0;
        right: 0;
      }
      .control-panel {
        background: #f9f9f9;
        padding: 15px;
        border-radius: 8px;
        margin-top: 20px;
      }
      .slider-container {
        margin: 10px 0;
      }
      label {
        display: block;
        margin-bottom: 5px;
        font-weight: bold;
        color: #555;
      }
      input[type="range"] {
        width: 100%;
      }
      .value-display {
        text-align: center;
        font-size: 14px;
        color: #777;
      }
      .code-example {
        background: #2d2d2d;
        color: #f8f8f2;
        padding: 15px;
        border-radius: 8px;
        margin-top: 20px;
        overflow-x: auto;
        font-family: "Fira Code", monospace;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>

    <script type="module">
      const { createApp, ref, reactive } = Vue;

      const App = {
        setup() {
          const items = ref([
            { id: 1, name: "美食", price: 25 },
            { id: 2, name: "饮料", price: 15 },
            { id: 3, name: "水果", price: 20 },
            { id: 4, name: "甜品", price: 18 },
            { id: 5, name: "快餐", price: 22 },
            { id: 6, name: "小吃", price: 12 },
          ]);

          const cart = reactive({
            count: 0,
            total: 0,
          });

          const balls = ref([]);
          const ballIndex = ref(0);
          const animationSpeed = ref(600);

          const addToCart = (item, event) => {
            cart.count++;
            cart.total += item.price;

            // 获取点击位置
            const startX = event.clientX;
            const startY = event.clientY;
            createBall(startX, startY);
          };

          // 创建小球
          const createBall = (startX, startY) => {
            let endEle = document
              .querySelector(".cart")
              .getBoundingClientRect();
            let endX = Math.floor(endEle.left + endEle.width / 2);
            let endY = Math.floor(endEle.top + endEle.height / 2);
            let fatherEle = document.querySelector(".container");
            let ball = document.createElement("div");
            ball.classList.add("ball");
            ball.style.left = startX + "px";
            ball.style.top = startY + "px";
            // 贝塞尔曲线路径
            ball.style.offsetPath = `path('M${0} ${0} C${100} ${-100}, ${
              endX - startX
            } ${endY - startY}, ${endX - startX} ${endY - startY}')`;

            fatherEle.appendChild(ball);
            setTimeout(() => {
              fatherEle.removeChild(ball);
            }, Number(animationSpeed.value) - 100);
            ball.animate(
              // 将偏移路径动画化
              { offsetDistance: [0, "100%"] },
              {
                duration: Number(animationSpeed.value),
                iterations: 1,
                easing: "cubic-bezier(.667,0.01,.333,.99)",
                direction: "alternate",
              }
            );
          };

          return {
            items,
            cart,
            balls,
            ballIndex,
            animationSpeed,
            addToCart,
            createBall,
          };
        },
        template: `
          <div class="container">
            <h1>美团购物车小球动画</h1>
            <p class="description">点击商品将生成飞向购物车的小球动画,多个小球实例互不干扰</p>

            <div class="items-container">
              <div
                v-for="item in items"
                :key="item.id"
                class="item"
                @click="addToCart(item, $event)"
              >
                <div class="item-img"></div>
                <div>{{ item.name }}</div>
                <div>¥{{ item.price }}</div>
              </div>
            </div>

            <div class="control-panel">
              <h3>动画控制</h3>
              <div class="slider-container">
                <label>动画速度: {{ animationSpeed }}ms</label>
                <input
                  type="range"
                  min="500"
                  max="1000"
                  v-model="animationSpeed"
                >
                <div class="value-display">调整小球飞行的速度</div>
              </div>
            </div>

          <div class="cart-container">
            <div class="cart">
              <span>购</span>
              <div class="cart-count">{{ cart.count }}</div>
            </div>
          </div>
        `,
      };

      const app = createApp(App);
      app.mount("#app");
    </script>
  </body>
</html>

结语

关注我,不迷路。

不定时分享前端相关问题以及解决方案。

希望能帮助每个在开发类似功能的小伙伴。

相关推荐
阿虎儿9 分钟前
TypeScript 内置工具类型完全指南
前端·javascript·typescript
IT_陈寒18 分钟前
Java性能优化实战:5个立竿见影的技巧让你的应用提速50%
前端·人工智能·后端
chxii1 小时前
6.3Element UI 的表单
javascript·vue.js·elementui
张努力1 小时前
从零开始的开发一个vite插件:一个程序员的"意外"之旅 🚀
前端·vue.js
远帆L1 小时前
前端批量导入内容——word模板方案实现
前端
Codebee1 小时前
OneCode3.0-RAD 可视化设计器 配置手册
前端·低代码
深兰科技1 小时前
深兰科技:搬迁公告,我们搬家了
javascript·人工智能·python·科技·typescript·laravel·深兰科技
葡萄城技术团队1 小时前
【SpreadJS V18.2 新版本】设计器新特性:四大主题方案,助力 UI 个性化与品牌适配
前端
lumi.1 小时前
Swiper属性全解析:快速掌握滑块视图核心配置!(2.3补充细节,详细文档在uniapp官网)
前端·javascript·css·小程序·uni-app