一根飞线的故事-SVG篇

简介: 作者|数澜UED团队 没有飞线的地图就像一个发际线上移的中年人一样平淡无奇。 —— By 胖子 每年春运和双十一的统计图都因为有飞线动效才更加吸引眼球,今天要为大家带来一根漂亮飞线要用什么姿势生成才能。

作者|数澜UED团队

没有飞线的地图就像一个发际线上移的中年人一样平淡无奇。 —— By 胖子

每年春运和双十一的统计图都因为有飞线动效才更加吸引眼球,今天要为大家带来一根漂亮飞线要用什么姿势生成才能。

SVG

本篇是主讲SVG来绘制飞线的,所以强大的SVG必定能完成我们绘制飞线效果的各种需求。首先我先为各位介绍下完成这根线需要用到的一些小知识点。

Path元素

path元素
是SVG基本形状中最强大的一个,它不仅能创建其他基本形状,还能创建更多其他形状。这里我们只需要用它来绘制一条曲线。

首先我们先创建好这根曲(tou)线(fa)。

OK,这根头发我们已经在屏幕上放好了,如果你将path元素的曲线无限放大会发现,其实它是由非常多的坐标点相互连接组成的。这个时候脑洞放一下,如果我们能获取到这些点是不是就是获取了线的绘制轨迹。就可以逐帧绘制飞线了动效了。


那要如何来获取和使用这些坐标点呢?

勤奋的查阅MDN,我发现这个问题强大的SVG已经帮我们解决了,可以使用getTotalLength和getPointAtLength这两个方法来搞定。

SVGPathElement.getTotalLength

但因为SVG中绘制的都是矢量图,所以path元素不存在是由若干个点构成的,所以调用该方法会返回该path元素从起始点到终点的总长度(浮点数)。

尽管和预期有所差别,但搭配上下面的getPointAtLength方法我们依然能完成之前预想的实现方法。

SVGPathElement.getPointAtLength

调用该方法会根据传入到起点的距离值来计算返回对应的path元素坐标点的位置x、y值。

通过组合使用这两方法,我们可以自己定义这段轨迹上有有多少个坐标点,并且可以获取对应这些点的坐标值。

下面我们使用D3来操作这些DOM节点获取对应的节点数据信息

首先我们需要先定义好飞线轨迹是由多少个点构成的:

const pointNum = 1500

接下来我们可以通过方法将获取到的轨迹总长度进行平分得到单位长度unit,然后再调用getPointAtLength获取对应距离的坐标值。

const pointNum = 1500
const path = d3.select('#line')
const pathline = path.node()
const totalLength = pathline.getTotalLength()
const points = []

const unit = totalLength / pointNum

for (let i = 0; i <= pointNum; i += 1) {
   
  points.push(pathline.getPointAtLength(i * unit))
}

接下来我们就可以通过这些数据绘制飞线动效了!

接下来我们就可以通过这些数据绘制飞线动效了!

接下来我们就可以通过这些数据绘制飞线动效了!

重要的话我们来强调三遍。

飞线动效-1

如下图,其实实现飞线具体头部深、尾部浅效果可以通过绘制若干透明度逐渐递减的圆来达到。(Echarts飞线使用类似思路)

接下来所需要做的就是让上面的飞线像下图的矩形一样,让它按照对应的轨迹路线来进行移动。

但由于飞线是由若干个圆重叠组成的,所以不能像矩形一样只需要控制一个元素的x、y值就搞定运动行为。尤其是如下图这样的曲线运动的情况。

为此我们需要声明一个飞线类,首先需要定义飞线的长度、样式速度等特性。

由于之前已经声明好该路径轨迹拆分成多少段了,所以在此我们取个巧定义飞线的长度是其中lineLen段的长度,设定速度为每次渲染移动speed段。

class FlyLine {
   
  totalNum = 1500
  lineLen = 150
  speed = 15
  radius = 2.5
  fill = 'rgb(255, 200, 65)'
  circles = []
  constructor(){
   
    // percent的用处会在后面体现
    this.percent = this.lineLen
  }
}

上面的说明看不懂?灵魂画手图片解析

定义好飞线的特性变量之后,接下来我们可以绘制具体的飞线了。

因为飞线是若干circle元素堆叠成的,所以我们在此提炼出一个公有的画圆方法:

class FlyLine {
   
  ...
  ...
  _drawCircle(cx, cy, i) {
   
    const {
   radius, circles, fill} = this
    if (circles[i]) {
   
      circles[i].attr("cx", cx).attr("cy", cy)
    } else {
   
      circles.push(
        circleG1
          .append("circle")
          .attr("cx", cx)
          .attr("cy", cy)
          .attr("r", radius)
          .attr("fill", fill)
          .attr('fill-opacity', i * 0.001)
      )
    }
  }
}

根据传入位置、索引值创建或更新circle元素的位置和元素的透明度。

现在我们来绘制第一个静态的飞线:

首先需要确定绘制飞线是由多少段小线段组成的(实际是由多少个圆相临近堆叠成的),接着我们就可以按照由浅及深的顺序开搞了。

class FlyLine {
   
  ...
  ...
  _drawFlyLine(){
   
    const {
   points, percent, lineLen} = this
    for (let i = percent - lineLen, j = 0; i < percent; i += 1, j += 1) {
   
      this._drawCircle(points[i].x, points[i].y, j)
    }
  }
 }

class FlyLine {
   
    ...
    ...
    animate() {
   
      const {
   lineLen, speed, totalNum} = this
      this._drawFlyLine()
      this.percent = this.percent + speed > totalNum ? len : this.percent + speed
      requestAnimationFrame(() => this.animate())
    }
  }

这下之前定义的percent就派上用场了

此时的percent就如同for循环中常用的i变量一样,逐渐自增speed,当到头就归零重新往复。

现在整个飞线动效的逻辑都清晰了:

FlyLine.animate方法本质上就是个复读机,一遍一遍的让percent变量由小到大变化,控制飞线由起始点到轨迹终点移动。

FlyLine._drawFlyLine方法的作用就是根据percent变量的值创建or更新飞线位置。

FlyLine._drawCircle就更不用说了,苦逼小弟,创建or更新circle元素的属性。percent变量更新一次,它要被苦逼的调用lineLen次。

现在这根飞线终于好好的动起来啦,真TM不容易。为了讲明白废了我不少(无用的)脑细胞。

当然,这个方法还不够完美,有许多需要优化的点,例如:

  1. 飞线的长度不能超过我们对轨迹分割的段数。

  2. 画一根飞线就要生成/更新几百个circle元素,浪费浏览器性能。

抛砖引玉,希望能够给大家提供一个好的思路来制作出更酷炫的飞线动效来。

飞线动效-2

算了,等不及你们来引玉了。我自己再继续开搞吧。

在上面提到的绘制一个飞线要上百个circle元素,这样非常浪费浏览器性能。

有没有好点的办法解决这个优秀前端不能忍受的痛呢?有!还真有!!

下面让我们开搞!!

我们知道NB的path元素可以绘制任意图形,上文中的飞线轨迹也是这样得到的。

这个时候我就在想了,D3相当NB了。它的过渡(transition)效果也是相当可以的。为什么我们不能直接拿来绘制飞线动效呢?

首先我们知道D3拥有attrTween这个属性过渡方法,我们可以在其中返回插值函数,根据传入的进度值不断变化元素的属性,呈现过渡动画效果。

现在先让我们用path画一根直线:

const path = container
  .append('path')
  .attr('fill', 'none')
  .attr('stroke', 'none')
  .attr('d', 'M50, 50  600, 50')
  .attr('id', 'line')

const pathline = path.node()
const len = pathline.getTotalLength()

const animate = () => {
   
  container.select('#flyline').remove()
  container.append('path')
  .attr('stroke', '#19D0DC')
  .attr('fill', 'none')
  .attr('id', 'flyline')
  .attr('stroke-width', '3px')
  .transition()
  .duration(5000)
  .attrTween('d', function(d) {
   
    const coord = path.attr('d').replace(/(M|Q)/g, '').match(/((\d|\.)+)/g)
    var x1 = +coord[0], y1 = +coord[1] // 起点
    return function(t) {
   
      const p = pathline.getPointAtLength(t * len)
      return `M${
     x1}, ${
     y1} ${
     p.x}, ${
     p.y}`
    }
  })
}

setInterval(animate, 5200)

已知直线路径长度和起点,并且这根线也不会拐弯,所以直接根据插值函数传入的进度值,通过使用getPointAtLength方法得到对应时刻的坐标值更新path元素的"d"属性即可。

直的搞定了,现在就是考验我们的时候了。我们需要使用熟练的技巧将耿直的它给掰弯了。

下图是一根二次贝塞尔曲线的绘制过程。因为轨迹已知,所以在各个阶段的起始点都是可以通过getPointAtLength方法获得的。唯一需要计算的只有不同阶段贝塞尔曲线控制点的位置。可以看到绘制它的过程中需要持续更新控制点,为此我去查了下二次贝塞尔曲线控制点的计算公式。

.attrTween('d', function(d) {
   
    const coord = path.attr('d').replace(/(M|Q)/g, '').match(/((\d|\.)+)/g)
    var x1 = +coord[0], y1 = +coord[1], // 起点
        x2 = +coord[2], y2 = +coord[3], // 控制点
        x3 = +coord[4], y3 = +coord[5]; // 终点
    return function(t) {
   
      const p = pathline.getPointAtLength(t * len)  
       // 根据插值方法进度实时计算当前控制点位置
      const x = (1 - t) * x1 + t * x2
      const y = (1 - t) * y1 + t * y2
      return `M${
     x1}, ${
     y1} Q${
     x},${
     y} ${
     p.x}, ${
     p.y}`
    }
  })

代码下过如下:

这根线终于能做到从头飞到尾了,但是尾巴有点长。这可急坏老父亲了,长残了将来可怎么找对象啊!?

别急,毕竟他是生在我中国的一根线。线丑不怕,美颜相机来凑啊!

我们可以用滤镜来先来帮它磨磨皮:

SVG为我们提供了蒙板遮罩等功能,我们只需要在蒙板中定义了一个透明度从内到外逐渐降低径向渐变的圆。然后让他一直跟着飞线的头移动就好了。

const mCircle = d3.select('#m-circle')
  .attrTween(function(d){
   
    ...
    const x = (1 - t) * x1 + t * x2
    const y = (1 - t) * y1 + t * y2
    mCircle.attr('cx', x)
      .attr('cy', y)
  })

美颜后的效果:

参考资料:

1. 地图与飞线

2. 贝塞尔曲线原理

参考DEMO链接:

https://codepen.io/Narcissus_Li

相关文章
|
算法
经典控制算法——PID算法原理分析及优化
这篇文章介绍了PID控制算法,这是一种广泛应用的控制策略,具有简单、鲁棒性强的特点。PID通过比例、积分和微分三个部分调整控制量,以减少系统误差。文章提到了在大学智能汽车竞赛中的应用,并详细解释了PID的基本原理和数学表达式。接着,讨论了数字PID的实现,包括位置式、增量式和步进式,以及它们各自的优缺点。最后,文章介绍了PID的优化方法,如积分饱和处理和微分项优化,以及串级PID在电机控制中的应用。整个内容旨在帮助读者理解PID控制的原理和实际运用。
3624 1
|
7月前
|
人工智能 自然语言处理 安全
AI检测工具的底层逻辑和技术原理分析
内容发布稳定性≠AI率低!真正影响展现的4大隐藏指标:结构、语义重复、主题偏移、表达轨迹。过度打磨反增机器感。推荐4层筛查+3份清单+5项快检,用可复盘流程替代单点优化,让写作成为可持续提效的生产过程。
443 3
|
JSON 人工智能 API
工具演变 & MCP基础
本内容介绍了大模型工具调用方式的演进历程,从早期的函数表达阶段到OpenAI的标准化工具定义,再到结构化推理的引入。同时深入解析了MCP(Model Context Protocol)的核心价值,即通过统一协议降低Agent与工具服务集成的工程复杂度,提升工具调用的标准化与效率。
536 0
|
监控 Java Shell
深入了解Spring Cloud Data Flow:流数据管道的管理和编排
在现代软件开发中,数据流处理变得越来越重要。随着数据量的不断增长,处理实时数据流以及批处理数据变得愈发复杂。为了更好地管理和编排数据流,Spring社区提供了一个强大的工具,即Spring Cloud Data Flow(SCDF)。本文将深入介绍Spring Cloud Data Flow的核心概念、架构和使用方式,以帮助您更好地理解和利用这个强大的数据流处理框架。
|
数据采集 资源调度 JavaScript
极致的灵活度满足工程美学:用Vue Flow绘制一个完美流程图
本文介绍了使用 Vue Flow 绘制流程图的方法与技巧。Vue Flow 是一个灵活强大的工具,适合自定义复杂的流程图。文章从环境要求(Node.js v20+ 和 Vue 3.3+)、基础入门案例、自定义功能(节点与连线的定制、事件处理)到实际案例全面解析其用法。重点强调了 Vue Flow 的高度灵活性,虽然预定义内容较少,但提供了丰富的 API 支持深度定制。同时,文中还分享了关于句柄(handles)的使用方法,以及如何解决官网复杂案例无法运行的问题。最后通过对比 mermaid,总结 Vue Flow 更适合需要高度自定义和复杂需求的场景,并附带多个相关技术博客链接供进一步学习。
|
Linux 应用服务中间件 nginx
若依修改---白名单怎样添加,怎样在初始化页面初始为自己的页面
若依修改---白名单怎样添加,怎样在初始化页面初始为自己的页面
若依修改---白名单怎样添加,怎样在初始化页面初始为自己的页面
|
自然语言处理 JavaScript 开发者
通义灵码插件:VSCode 的智能编程助手
通义灵码插件:VSCode 的智能编程助手
18073 5

热门文章

最新文章