高德室外多点实时轨迹展示 + 搜索选点 + canvas路径轨迹展示 + 计算角度+平滑移动动画

679 阅读3分钟

需要实现三个功能: 室外多机器人实时轨迹展示(高德) 搜索选点(高德) 室内图轨迹展示(canvas)

高德地图基本概念:

地图 Amap和实例map

AMap 对象提供了加载地图、创建地图、添加覆盖物、进行地图操作,加载插件等等的方法和属性。可以使用AMap创建地图实例。

const map = new AMap.Map(mapcontainer, {
  center: [121.412577, 31.218804], // Center coordinates
  zoom: 14, // Initial zoom level
});

地图实例是通过 AMap.Map 创建的一个具体的地图对象。它代表了一个可视化的地图窗口,你可以在上面添加标记、覆盖物、控件等等。地图实例具有与地图相关的属性和方法,例如中心点坐标、缩放级别、平移、添加覆盖物等等。

AMap 是高德地图 JavaScript API 的主要入口,提供了加载地图和各种功能的方法,而地图实例则是你在页面上创建的具体的地图对象,你可以通过它进行地图的操作和交互。

就是点。可以通过调用API进行添加,修改内容,移动,设置点击事件等。这里的逻辑很简单,就是把路径数据的第一个点的位置初始化为一个Marker,后续都是这个Marker的移动,设置点击事件和样式,获取坐标等。

但是动画效果需要自己写,在这个案例中,我设置了自动计算角度也是无效的,同样需要自己计算(这个暂时还没搞清楚为什么)。 路径没用到,就不说了。

室外多机器人实时轨迹展示(高德)

整体分为三个部分:

  1. 初始化地图 (useInitMap)
  2. 获取路径数据(可以从webSocket中获取) (usePathData)
  3. 绘制点 (useMarker)
  4. 让点动起来 计算动画和角度 使之看起来平滑移动 (useMarkAnimations)

这里有两个方案,一个是巡航(主要是路径,DEMO就不放这里了),一个是Marker(需要自己移动点),巡航能支持更复杂的交互展示,两个方案都写了DEMO。这里因为不涉及轨迹展示,所以用Marker实现比较简单。客服回复如下:

image.png

image.png

index.vue

<template>
  <div class="page-tenant-map">
    <RobotInfoCardComponent :robotId="selectRobotId" /> 
    <div id="mapcontainer" style="width: 100%; height: 800px"></div>
  </div>
</template>

<script setup lang="ts">
  import RobotInfoCardComponent from '../RobotInfoCardComponent/index.vue';
  import ReviewRobotDataCardComponent from './components/ReviewRobotDataCardComponent/index.vue';
  import useInitMap from './compositions/useInitMap';
  import { onMounted, ref } from 'vue';
  import AMapLoader from '@amap/amap-jsapi-loader';
  import usePathData from '@/views/tenant/map/components/OutSideMapComponent/compositions/usePathData';
  import useMarker from '@/views/tenant/map/components/OutSideMapComponent/compositions/useMarker';
  import useMarkAnimations from '@/views/tenant/map/components/OutSideMapComponent/compositions/useMarkAnimations';

  const selectRobotId = ref('');
  const AMapKey = '';

  onMounted(async () => {
    // Load AMap API
    const AMap = await AMapLoader.load({
      key: AMapKey,
      version: '2.0', 
    });

    const { map } = useInitMap(AMap, 'mapcontainer');

    const { pathData } = usePathData();
    const { markers } = useMarker(map, AMap, pathData, selectRobotId);

    useMarkAnimations(map, AMap, pathData, markers);
  });
</script>

<style scoped src="./index.less" lang="less" />

useInitMap 初始化地图 加载一些插件

const useInitMap = (AMap, mapcontainer: string) => {
  const map = new AMap.Map(mapcontainer, {
    center: [121.412577, 31.218804], // Center coordinates
    zoom: 14, // Initial zoom level
  });

  AMap.plugin(
    [
      'AMap.ToolBar',
      'AMap.Scale',
      'AMap.Geolocation',
      'AMap.PlaceSearch',
      'AMap.Geocoder',
      'AMap.MoveAnimation',
    ],
    () => {
      // 缩放条
      const toolbar = new AMap.ToolBar();
      // 比例尺
      const scale = new AMap.Scale();

      map.addControl(toolbar);
      map.addControl(scale);
    },
  );

  return { map };
};

export default useInitMap;

usePathData 随便生成的两个路径数据,一个圆的一个方的,测试角度用,注释中是从webSocket连接中获取数据

import { useWebSocket } from '@vueuse/core';
import { reactive, watchEffect } from 'vue';

const usePathData = () => {
  // const state = reactive({
  //   server: 'ws://localhost:3300/test',
  //   sendValue: '',
  //   recordList: [] as { id: number; time: number; res: string }[],
  // });
  //
  // const { status, data, send, close, open } = useWebSocket(state.server, {
  //   autoReconnect: false,
  //   heartbeat: true,
  // });
  //
  // // 解码数据 返回
  // watchEffect(() => {
  //   if (data.value) {
  //     try {
  //       const res = JSON.parse(data.value);
  //       state.recordList.push(res);
  //     } catch (error) {
  //       state.recordList.push({
  //         res: data.value,
  //         id: Math.ceil(Math.random() * 1000),
  //         time: new Date().getTime(),
  //       });
  //     }
  //   }
  // });

  // 初始的经纬度
  const center = { lng: 121.422635, lat: 31.216688 };

  // 生成缓慢前进的四方形路径的经纬度数据数组
  function generateSlowSquarePath(center, sideLength, numPoints, distancePerStep) {
    const halfSide = sideLength / 2;
    const path = [];

    for (let i = 0; i < numPoints; i++) {
      const lng = center.lng + (i % 2 === 0 ? halfSide : -halfSide);
      const lat = center.lat + (i < 2 ? halfSide : -halfSide);
      path.push({
        lng,
        lat,
        extData: {
          markerId: 234,
        },
      });
    }

    const slowPath = [];
    for (let i = 0; i < path.length - 1; i++) {
      const start = path[i];
      const end = path[i + 1];
      for (let j = 0; j < distancePerStep; j++) {
        const lng = start.lng + ((end.lng - start.lng) * j) / distancePerStep;
        const lat = start.lat + ((end.lat - start.lat) * j) / distancePerStep;
        slowPath.push({
          lng,
          lat,
          extData: {
            markerId: 567,
          },
        });
      }
    }

    slowPath.push(path[path.length - 1]);

    return slowPath;
  }

  const slowSquarePath = generateSlowSquarePath(center, 0.001, 4, 10);

  // 生成缓慢前进的圆形路径的经纬度数据数组
  function generateSlowCircularPath(center, radius, numPoints, distancePerStep) {
    const path = [];

    for (let i = 0; i < numPoints; i++) {
      const angle = (i / numPoints) * 2 * Math.PI;
      const lng = center.lng + radius * Math.cos(angle);
      const lat = center.lat + radius * Math.sin(angle);
      path.push({
        lng,
        lat,
        extData: {
          markerId: i,
        },
      });
    }

    const slowPath = [];
    for (let i = 0; i < path.length - 1; i++) {
      const start = path[i];
      const end = path[i + 1];
      for (let j = 0; j < distancePerStep; j++) {
        const lng = start.lng + ((end.lng - start.lng) * j) / distancePerStep;
        const lat = start.lat + ((end.lat - start.lat) * j) / distancePerStep;
        slowPath.push({
          lng,
          lat,
          extData: {
            markerId: i,
          },
        });
      }
    }

    slowPath.push(path[path.length - 1]);

    return slowPath;
  }

  const slowCircularPath = generateSlowCircularPath(center, 0.01, 50, 50);

  const pathData = [[...slowSquarePath], [...slowCircularPath]];

  return {
    pathData,
  };
};

export default usePathData;

useMarker 主要是初始化点,设置一些点击事件和样式效果,setFitView是让地图正好包下所有的点。

import { ref } from 'vue';

const useMarker = (map, AMap, pathData, selectRobotId) => {
  let currentSelectMarker = null;
  const markers = ref([]);
  // Create and add markers to the map based on pathData

  pathData.forEach((path) => {
    const marker = new AMap.Marker({
      title: '配送001\n' + '\n' + '室外,离线\n' + '\n' + '62%,10km/h',
      position: [path[0].lng, path[0].lat],
      map,
      icon: new AMap.Icon({
        imageSize: new AMap.Size(60, 60),
        image:
          'https://files.axshare.com/gsc/NNGZ7Q/32/bd/f0/32bdf06cae754929be938453c4494f91/images/%E7%A7%9F%E6%88%B7%E5%85%A8%E5%B1%80%E5%9C%B0%E5%9B%BE/u72.svg?pageId=126b2c69-7f1f-4de1-bc41-3f0f0487c1d6',
      }),
      autoRotation: true,
      animation: 'AMAP_ANIMATION_DROP',
      extData: {
        a: 1,
        ...path[0].extData,
      },
    });

    marker.on('click', (data) => {
      console.log('click', data, marker.getExtData());
      selectRobotId.value = marker.getExtData()?.markerId;

      const noSelectIcon = new AMap.Icon({
        imageSize: new AMap.Size(60, 60),
        image:
          'https://files.axshare.com/gsc/NNGZ7Q/32/bd/f0/32bdf06cae754929be938453c4494f91/images/%E7%A7%9F%E6%88%B7%E5%85%A8%E5%B1%80%E5%9C%B0%E5%9B%BE/u72.svg?pageId=126b2c69-7f1f-4de1-bc41-3f0f0487c1d6',
      });

      currentSelectMarker && currentSelectMarker.setIcon(noSelectIcon);

      const selectIcon = new AMap.Icon({
        imageSize: new AMap.Size(60, 60),
        image:
          'https://files.axshare.com/gsc/NNGZ7Q/32/bd/f0/32bdf06cae754929be938453c4494f91/images/%E7%A7%9F%E6%88%B7%E5%85%A8%E5%B1%80%E5%9C%B0%E5%9B%BE/u75.svg?pageId=126b2c69-7f1f-4de1-bc41-3f0f0487c1d6',
      });

      marker.setIcon(selectIcon);

      currentSelectMarker = marker;
    });


    markers.value.push(marker);
  });

  map.setFitView(null, true, [220, 220, 220, 220]);

  return {
    markers,
  };
};

export default useMarker;

useMarkAnimations 根据路径 让点平滑移动 (其实有点担心,如果是实时数据获取,网络不稳定的情况下,不处理数据的话,会不会出现很离谱的延迟,放在二期做吧)

const useMarkAnimations = (map, AMap, pathData, markers) => {
  const animationInterval = 1000; 
  let currentIndex = 0;

  setInterval(() => {
    markers.value.forEach((marker, markerIndex) => {
      const path = pathData[markerIndex];
      const nextIndex = (currentIndex + 1) % path.length;
      const nextPoint = path[nextIndex];

      const startPosition = marker.getPosition();
      const endPosition = new AMap.LngLat(nextPoint.lng, nextPoint.lat);

      animateMarker(AMap, marker, startPosition, endPosition);
    });

    currentIndex = (currentIndex + 1) % pathData[0].length;
  }, animationInterval);
  
  function animateMarker(AMap, marker, startPosition, endPosition) {
    const startTime = new Date().getTime();
    const startRotation = marker.getAngle(); // 获取起始角度

    const getAngle = (startPoint, endPoint) => {
      if (!(startPoint && endPoint)) {
        return 0;
      }
      let dRotateAngle = Math.atan2(
        Math.abs(startPoint.lng - endPoint.lng),
        Math.abs(startPoint.lat - endPoint.lat),
      );
      if (endPoint.lng >= startPoint.lng) {
        if (endPoint.lat >= startPoint.lat) {
        } else {
          dRotateAngle = Math.PI - dRotateAngle;
        }
      } else {
        if (endPoint.lat >= startPoint.lat) {
          dRotateAngle = 2 * Math.PI - dRotateAngle;
        } else {
          dRotateAngle = Math.PI + dRotateAngle;
        }
      }
      dRotateAngle = (dRotateAngle * 180) / Math.PI;
      return dRotateAngle;
    };

    function step() {
      const currentTime = new Date().getTime();
      const progress = (currentTime - startTime) / 1000;

      if (progress < 1) {
        const lng = startPosition.lng + (endPosition.lng - startPosition.lng) * progress;
        const lat = startPosition.lat + (endPosition.lat - startPosition.lat) * progress;

        const rotation = getAngle(startPosition, endPosition);

        marker.setAngle(rotation);

        marker.setPosition(new AMap.LngLat(lng, lat));

        requestAnimationFrame(step);
      } else {
        marker.setPosition(endPosition);
        marker.setAngle(startRotation); // 完成后还原角度
      }
    }

    step();
  }
};

export default useMarkAnimations;

搜索选点(高德)

效果图: image.png index.vue 先放着 稍后拆分

<template>
  <div class="map-wrapper">
    <div id="mapcontainer"></div>
    <div class="search-box">
      <a-auto-complete
        v-model:value="keyword"
        style="width: 200px"
        placeholder="输入城市+关键字搜索"
        @select="handleSelect"
        @search="handleSearch"
        :trigger-on-focus="false"
        clearable
        :options="options"
      />
      <a-input
        v-model:value="location.longitude"
        placeholder="点击地图选择经度"
        maxlength="15"
        disabled
        style="width: 150px; margin: 0 5px"
      />
      <a-input
        v-model:value="location.latitude"
        placeholder="点击地图选择纬度"
        maxlength="15"
        disabled
        style="width: 150px"
      />

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/231502519a1d4431ba93135e4dfd41f3~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2706&h=1282&s=1802237&e=png&b=f4f3f3)
      <a-button
        type="primary"
        v-if="location.longitude && location.latitude"
        style="width: 150px; margin: 0 5px"
        @click="handleConfirm"
        >选择该位置
      </a-button>
    </div>
  </div>
</template>

<script setup lang="ts">
  import { ref, onMounted, watch } from 'vue';
  import { shallowRef } from 'vue';

  import AMapLoader from '@amap/amap-jsapi-loader';

  window._AMapSecurityConfig = {
    securityJsCode: '',
  };
  const props = defineProps({
    location: {
      type: Object,
      default() {
        return {};
      },
    },
  });
  const emit = defineEmits(['update:modelValue']);
  const map = shallowRef(null);
  const options = ref(null);

  const location = ref(props.location);

  const handleConfirm = () => {
    emit('selectLocation', location.value);
  };

  watch(location, (val) => {
    if (val.longitude && val.latitude) {
      drawMarker();
    }
  });

  const keyword = ref('');
  let placeSearch, AMapObj, marker, geocoder;

  function initMap() {
    AMapLoader.load({
      key: '', // 申请好的Web端Key,首次调用 load 时必填
      version: '2.0',
    }).then((AMap) => {
      AMapObj = AMap;
      map.value = new AMap.Map('mapcontainer');
      // 添加点击事件
      map.value.on('click', onMapClick);
      if (location.value.longitude) {
        drawMarker();
      }
      AMap.plugin(
        [
          'AMap.ToolBar',
          'AMap.Scale',
          'AMap.Geolocation',
          'AMap.PlaceSearch',
          'AMap.Geocoder',
          'AMap.AutoComplete',
        ],
        () => {
          // 缩放条
          const toolbar = new AMap.ToolBar();
          // 比例尺
          const scale = new AMap.Scale();
          // 定位
          const geolocation = new AMap.Geolocation({
            enableHighAccuracy: true, //是否使用高精度定位,默认:true
            timeout: 10000, //超过10秒后停止定位,默认:5s
            position: 'RT', //定位按钮的停靠位置
            buttonOffset: new AMap.Pixel(10, 20), //定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20)
            zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
          });
          geocoder = new AMap.Geocoder({
            city: '全国',
          });
          map.value.addControl(geolocation);
          map.value.addControl(toolbar);
          map.value.addControl(scale);
          placeSearch = new AMap.PlaceSearch({
            map: map.value,
            city: '',
            pageSize: 10, // 单页显示结果条数
            pageIndex: 1, // 页码
            citylimit: false, // 是否强制限制在设置的城市内搜索
            autoFitView: true,
          });

          placeSearch.on('markerClick', (item) => {
            console.log('markerClick', item.data);

            const { pname, cityname, adname, address, name } = item?.data;
            const { lng, lat } = item.data?.location;
            location.value = {
              longitude: lng,
              latitude: lat,
              address,
              zone: [pname, cityname, adname],
              name,
            };
            map.value?.setZoomAndCenter(16, [lng, lat]);
          });
        },
      );
    });
  }

  onMounted(() => {
    initMap();
  });

  // 搜索地图
  function handleSearch(queryString, cb) {
    placeSearch.search(queryString, (status, result) => {
      if (result && typeof result === 'object' && result.poiList) {
        const list = result.poiList.pois;
        list.forEach((item) => {
          item.value = item.name;
          item.label = item.name;
        });
        cb?.(list);

        options.value = list;
      } else {
        cb?.([]);
      }
    });
  }

  // 点击地图
  function onMapClick(e) {
    const { lng, lat } = e.lnglat;

    // 逆地理编码
    geocoder.getAddress([lng, lat], (status, result) => {
      if (status === 'complete' && result.info === 'OK') {
        const { addressComponent, formattedAddress } = result.regeocode;
        let { city, province, district } = addressComponent;
        if (!city) {
          // 直辖市
          city = province;
        }

        console.log(
          location.value,
          'location',
          lng,
          lat,
          formattedAddress,
          province,
          city,
          district,
        );

        location.value = {
          longitude: lng,
          latitude: lat,
          address: formattedAddress,
          zone: [province, city, district],
        };
      }
    });
  }

  // 点击搜索项
  function handleSelect(label, item) {
    const { pname, cityname, adname, address, name } = item;
    const { lng, lat } = item.location;

    location.value = {
      longitude: lng,
      latitude: lat,
      address,
      zone: [pname, cityname, adname],
      name,
    };
    map.value?.setZoomAndCenter(16, [lng, lat]);
  }

  // 绘制地点marker
  function drawMarker(val) {
    const { longitude, latitude } = location.value || val;
    if (marker) {
      marker.setMap(null);
    }

    marker = new AMapObj.Marker({
      position: new AMapObj.LngLat(longitude, latitude),
      anchor: 'bottom-center',
      clickable: true,
    });

  
    map.value?.add(marker);
    map.value?.setZoomAndCenter(16, [longitude, latitude]);
  }
</script>

<style lang="less" src="./index.less" scoped />

室内图轨迹展示(canvas)

目标:多个图标点根据实时按秒获取的坐标移动及转角,使用requestAnimationFrame来做平滑过渡。 由于canvas绘制,元素的点击事件设置只能根据点击坐标去做,案例中的点移动速度和方向不定,测试发现点击位置和点的当前位置总是差100-300距离,所以采用外部控制选中ID,绘制时判断选中的数据ID选择绘制图标。 path2D有可能可以实现,时间关系,之后再研究。

效果图: image.png

关键点:

  1. const distance = Math.sqrt(dx * dx + dy * dy);平方根计算两点之间距离
  2. 先清除画布再画所有点
  3. Math.atan2(y2 - y1, x2 - x1) + Math.PI / 2; 计算两点之间的角度

思路:画出一个矩形和圆形的路径数据,按秒筛出每个点的当前位置和下一个位置组成一个数组。然后去清除画布,绘制所有点,使用requestAnimationFrame将距离等分,计算出当前时间节点的位置。

index.vue

<template>
  <div class="inside-map-component">
    <a-radio-group v-model:value="selectRobotId" class="select-robot">
      <a-radio-button :value="1">机器人1</a-radio-button>
      <a-radio-button :value="2">机器人2</a-radio-button>
    </a-radio-group>

    <RobotInfoCardComponent :robotId="selectRobotId" v-if="!!selectRobotId" />

    <div class="inside-canvas">
      <canvas ref="canvasRef" id="insideMap" width="1200" height="800"></canvas>
    </div>
  </div>
</template>

<script setup lang="ts">
  import RobotInfoCardComponent from '../RobotInfoCardComponent/index.vue';
  import { ref } from 'vue';
  import Segmented from '@/components/Segmented/index.vue';
  import { useCanvasAnimation } from './compositions/useCanvasAnimation';
  import { usePathData } from '@/views/tenant/map/components/InSideMapComponent/compositions/usePathData';

  const selectRobotId = ref(1);
 

  const { pointData } = usePathData();
  const { canvasRef } = useCanvasAnimation(pointData, selectRobotId);
</script>

<style src="./index.less" scoped lang="less"></style>

usePathData:

import { ref } from 'vue';

export function usePathData() {
  const pointData = ref([
    {
      path: generateCircularPath(400, 300, 250, 36), // 360 points for a complete circle
    },
    {
      path: generateRectanglePath(400, 300, 200, 200), // No need to specify numPoints for square
    },
  ]);
  console.log('pointData1', pointData);

  function generateCircularPath(cx, cy, radius, numPoints) {
    const path = [];
    for (let i = 0; i < numPoints; i++) {
      const angle = (i / numPoints) * Math.PI * 2;
      const x = cx + radius * Math.cos(angle);
      const y = cy + radius * Math.sin(angle);
      path.push([
        x,
        y,
        {
          id: 2,
        },
      ]);
    }
    return path;
  }

  function generateRectanglePath(x: number, y: number, width: number, height: number) {
    const path = [[400, 300]];
    const numPointsPerSide = 10; // Number of points for each side of the rectangle

    // Generate points along the left side of the rectangle
    for (let i = 1; i <= numPointsPerSide; i++) {
      const progress = i / numPointsPerSide;
      const pointX = x;
      const pointY = y + progress * height;
      path.push([
        pointX,
        pointY,
        {
          id: 1,
        },
      ]);
    }

    // Generate points along the top side of the rectangle
    for (let i = 1; i <= numPointsPerSide; i++) {
      const progress = i / numPointsPerSide;
      const pointX = x + progress * width;
      const pointY = path[path.length - 1][1];
      path.push([
        pointX,
        pointY,
        {
          id: 1,
        },
      ]);
    }

    // Generate points along the right side of the rectangle
    for (let i = 0; i <= numPointsPerSide; i++) {
      const progress = i / numPointsPerSide;
      const pointX = x + width;
      const pointY = path[path.length - 1][1] + progress * height;
      path.push([
        pointX,
        pointY,
        {
          id: 1,
        },
      ]);
    }

    // Generate points along the bottom side of the rectangle
    for (let i = 0; i <= numPointsPerSide; i++) {
      const progress = i / numPointsPerSide;
      const pointX = path[path.length - 1][0] + width - progress * width;
      const pointY = y + height;
      path.push([
        pointX,
        pointY,
        {
          id: 1,
        },
      ]);
    }

    return path;
  }

  return {
    pointData,
  };
}

useCanvasAnimation:

import { ref, onMounted, onUnmounted } from 'vue';

interface Point {
  path: Array<[number, number]>;
}

const ICON_SIZE = 60;

export function useCanvasAnimation(data: Point[], selectRobotId) {
  let setIntervalId;
  const canvasRef = ref<HTMLCanvasElement | null>(null);
  const pointsToDraw = ref([]);

  const iconImage = new Image();
  iconImage.src = '@/assets/svg/robot/robotPoint.svg'; // Replace with the actual path
  iconImage.width = ICON_SIZE;
  iconImage.height = ICON_SIZE;

  const activeIconImage = new Image();
  activeIconImage.src = '@/assets/svg/robot/robotPointActive.svg'; // Replace with the actual path
  activeIconImage.width = ICON_SIZE;
  activeIconImage.height = ICON_SIZE;

  const start = () => {
    const animationInterval = 1000; // Adjust as needed
    let currentIndex = 1;

    setIntervalId = setInterval(() => {
      console.log(selectRobotId, 'selectRobotId');
      pointsToDraw.value = data.value
        .map((point) => {
          const path = point.path;
          const startPoint = path[currentIndex - 1];
          const endPoint = path[currentIndex];

          if (startPoint && endPoint) {
            return { startPoint, endPoint };
          }

          return null;
        })
        .filter((point) => point !== null);

      // 找出这一帧所有的点 组成一个点数组 去绘制

      animate();

      currentIndex = currentIndex + 1;
    }, animationInterval);
  };

  onMounted(() => {
    canvasRef.value && start();
  });

  onUnmounted(() => {
    clearInterval(setIntervalId);
  });

  // 计算移动角度
  function calculateAngle(x1: number, y1: number, x2: number, y2: number) {
    return Math.atan2(y2 - y1, x2 - x1) + Math.PI / 2;
  }

  // 缓动函数 不一定用的上
  function easeInOutCubic(t) {
    return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
  }

  // 绘制动画
  const animate = () => {
    // console.log('startPoint', startPoint, endPoint);
    const canvas = canvasRef.value;
    const ctx = canvas?.getContext('2d');

    if (!ctx || !canvas) return;
    const startTime = new Date().getTime();

    const step = () => {
      const currentTime = new Date().getTime();
      const progress = (currentTime - startTime) / 1000;

      // 清除画布
      ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
      if (progress <= 1) {
        // 绘制所有点
        pointsToDraw.value.forEach((point) => {
          const { startPoint, endPoint } = point;
          const [startX, startY] = startPoint; // Get the next point in the path
          const [targetX, targetY] = endPoint; // Get the next point in the path

          const dx = targetX - startX;
          const dy = targetY - startY;
          const distance = Math.sqrt(dx * dx + dy * dy);

          ctx.save();

          let finallyX = startX;
          let finallyY = startY;

          if (distance > 0) {
            const step = progress * distance;
            const factor = step / distance;
            finallyX = startX + dx * factor;
            finallyY = startY + dy * factor;
          }

          ctx.translate(finallyX, finallyY);

          ctx.rotate(calculateAngle(startX, startY, targetX, targetY));

          ctx.drawImage(
            selectRobotId.value === startPoint[2]?.id ? activeIconImage : iconImage,

            -ICON_SIZE / 2,
            -ICON_SIZE / 2,
            ICON_SIZE,
            ICON_SIZE,
          );
          ctx.restore();
        });

        requestAnimationFrame(step);
      }
    };

    step();
  };

  return {
    canvasRef,
    pointsToDraw,
  };
}