SVG 实现飞线功能

svg 实现如图飞功能,具体代码如下:

vue 中使用

html 复制代码
<template>
  <div>
   <span id="dv-1">测试</span>
   
   <span id="dv-2" style="margin:100px;">测试</span>
  </div>
  <fly-line2 :fly-list="flyLineList"></fly-line2>
</template>

<script lang="ts">

import Flyline2 from '@/components/FlylineChartEnhanced/FlyLine2.vue'
import {type FlyLineItem } from '@/api/types/RespTypes'

export default {
  name: 'home',
  components: {
    Flyline2
  },
  setup() {
  
    const flyLineList = ref<FlyLineItem[]>([])

    function getCenter(el: HTMLElement) {
      const rect = el.getBoundingClientRect()
      return {
        x: rect.left + rect.width / 2,
        y: rect.top + rect.height / 2
      }
    }

    function test(){
      nextTick(()=>{
        const el  = document.getElementById("dv-1")
        const el2 = document.getElementById("dv-2")
        const form = getCenter(el)
        const to = getCenter(el2)
        flyLineList.value.push({
          id: "line_1",
          from: [form.x, form.y],
          to: [to.x, to.y]
        })
      })
    }

    onMounted(() => {
      test()
    })

    return {
      flyLineList
    }
  },
}
</script>

<style lang="scss" scoped>

</style>

组件 FlyLine2.vue

html 复制代码
<template>
  <svg  class="svg-fly-line" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <radialGradient id="lineGradient" cx="50%" cy="50%" r="50%">
        <stop offset="0%" stop-color="#fff" stop-opacity="1"></stop>
        <stop offset="100%" stop-color="#fff" stop-opacity="0"></stop>
      </radialGradient>

      <linearGradient id="lineGradient1" x1="91.93%" y1="77.23%" x2="8.07%" y2="22.77%">
        <!-- 12.37% 处为透明蓝色 -->
        <stop offset="12.37%" stop-color="#00A0E9" stop-opacity="0" />
        <!-- 86.15% 处为不透明蓝色 -->
        <stop offset="86.15%" stop-color="#00A0E9" stop-opacity="1" />
      </linearGradient>

    </defs>

    <!-- 遍历多条飞线路径 -->
    <g v-for="(item, index) in lineLst" :key="index" :data-index="index">
      <!-- 动态路径 -->
      <path :id="`flyPath${index}`" :d="getFlyPath(item)" fill="transparent"></path>
      <!-- 遮罩 -->
      <mask :id="`flyMask${index}`">
        <!-- 拖尾 r 长度  -->
        <circle cx="0" cy="0" r="60" fill="url(#lineGradient)">
          <animateMotion :dur="`${item.speed || 5000}ms`" :path="getFlyPath(item)" rotate="auto" repeatCount="indefinite"></animateMotion>
        </circle>
      </mask>
      <!-- 底层轨迹线 -->
      <use :xlink:href="`#flyPath${index}`" stroke-width="8" stroke="rgba(255, 255, 255, .1)"></use>
      <!-- 渐变流光拖尾 → from/to 自动计算 -->
      <use :xlink:href="`#flyPath${index}`" stroke-width="6" stroke="#00A0E9" :mask="`url(#flyMask${index})`">
        <animate
          attributeName="stroke-dasharray"
          :from="`0, ${item.pathLength}`"
          :to="`${item.pathLength}, 0`"
          :dur="`${item.speed || 5000}ms`"
          repeatCount="indefinite"
        ></animate>
      </use>

      <!-- 箭头 -->
      <path d="M0,-4 L8,0 L0,4 Z" fill="#00A0E9">
        <animateMotion
          :dur="`${item.speed || 5000}ms`"
          :path="getFlyPath(item)"
          rotate="auto"
          repeatCount="indefinite"
        />
      </path>
    </g>
  </svg>
</template>

<script setup lang="ts">
import { defineProps ,getCurrentInstance} from 'vue'

// 单条飞线类型
export interface FlyLineItem {
  from: number[] // 起点 x,y坐标
  to: number[] // 终点 x
  curve?: number // 弧线偏移 正负控制上下弯曲
  lineColor?: string // 轨迹颜色
  lineWidth?: number // 轨迹宽度
  arrowFill?: string // 箭头填充色
  arrowScale?: number // 箭头缩放大小
  speed?: number // 飞行时长 ms
  pathLength?:number
}

//@ts-ignore
const { ctx: that } = getCurrentInstance()

const props = defineProps<{
  flyList: FlyLineItem[]
}>()

const lineLst =ref<FlyLineItem[]>([])

watch(()=>props.flyList,async (nVal,oVal)=>{
  await nextTick()
  lineLst.value = props.flyList
  lineLst.value.forEach((item, index) => {
    const path = document.getElementById(`flyPath${index}`)
    if (path) {
      //@ts-ignore
      const length= path.getTotalLength()
      console.log('[FlyLine2#getPathLength:72]:', `${length}`)
      item.pathLength = length
    }
  })
},{deep:true,immediate:true})

// 生成贝塞尔曲线路径
const getFlyPath = (item: FlyLineItem) => {
  const { from, to, curve = -70 } = item
  const centerX = (from[0] + to[0]) / 2
  const centerY = (from[1] + to[1]) / 2 + curve
  //d="M 起点X 起点Y Q 控制点X 控制点Y 终点X 终点Y"
  return `M${from[0]},${from[1]} Q${centerX},${centerY} ${to[0]},${to[1]}`
}

async function getPathLength(idx: number) {
  await nextTick()
  //@ts-ignore
  const path:SVGPathElement|null = document.getElementById(`flyPath${idx}`)
  if (path) {
    const length= path.getTotalLength()
    console.log('[FlyLine2#getPathLength:108]:', `${length}`)
    return length
  }
  return 0
}

// 生成贝塞尔路径
// function getPathD(p1: { x: number, y: number }, p2: { x: number, y: number }, curveOffset = -80) {
//   const mx = (p1.x + p2.x) / 2
//   const my = (p1.y + p2.y) / 2 + curveOffset
//   return `M ${p1.x} ${p1.y} Q ${mx} ${my} ${p2.x} ${p2.y}`
// }

// 获取元素中心点
function getCenter(el: HTMLElement) {
  const rect = el.getBoundingClientRect()
  return {
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
  }
}

function handleResize() {
  lineLst.value.forEach((item, index) => {
    //@ts-ignore
    const path:SVGPathElement|null = document.getElementById(`flyPath${index}`)
    if (path) {
      const length= path.getTotalLength()
      item.pathLength = length
    }
  })
  that.$forceUpdate()
}

// 挂载
onMounted(() => {
  window.addEventListener('resize', handleResize)
})
// 销毁
onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
</script>

<style scoped lang="scss">
.svg-fly-line {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  z-index: 1;
}
</style>
相关推荐
卸任14 分钟前
Vibe coding工具使用心得
前端·ai编程
brien91 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易2 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding2 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马2 小时前
前沿视界
前端
kyriewen2 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员