一根飛線的故事-SVG篇

做者|數瀾UED團隊html

沒有飛線的地圖就像一個髮際線上移的中年人同樣平淡無奇。 —— By 胖子前端

每一年春運和雙十一的統計圖都由於有飛線動效才更加吸引眼球,今天要爲你們帶來一根漂亮飛線要用什麼姿式生成才能。node

SVG

本篇是主講SVG來繪製飛線的,因此強大的SVG一定能完成咱們繪製飛線效果的各類需求。首先我先爲各位介紹下完成這根線須要用到的一些小知識點。瀏覽器

Path元素app

path元素
是SVG基本形狀中最強大的一個,它不只能建立其餘基本形狀,還能建立更多其餘形狀。這裏咱們只須要用它來繪製一條曲線。ide

首先咱們先建立好這根曲(tou)線(fa)。函數

一根飛線的故事-SVG篇

OK,這根頭髮咱們已經在屏幕上放好了,若是你將path元素的曲線無限放大會發現,其實它是由很是多的座標點相互鏈接組成的。這個時候腦洞放一下,若是咱們能獲取到這些點是否是就是獲取了線的繪製軌跡。就能夠逐幀繪製飛線了動效了。性能

一根飛線的故事-SVG篇
那要如何來獲取和使用這些座標點呢?優化

勤奮的查閱MDN,我發現這個問題強大的SVG已經幫咱們解決了,可使用getTotalLengthgetPointAtLength這兩個方法來搞定。動畫

[SVGPathElement.getTotalLength](https://developer.mozilla.org/en-US/docs/Web/API/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飛線使用相似思路)

一根飛線的故事-SVG篇

接下來所須要作的就是讓上面的飛線像下圖的矩形同樣,讓它按照對應的軌跡路線來進行移動。

一根飛線的故事-SVG篇

但因爲飛線是由若干個圓重疊組成的,因此不能像矩形同樣只須要控制一個元素的xy值就搞定運動行爲。尤爲是以下圖這樣的曲線運動的狀況。

一根飛線的故事-SVG篇

爲此咱們須要聲明一個飛線類,首先須要定義飛線的長度、樣式速度等特性。

因爲以前已經聲明好該路徑軌跡拆分紅多少段了,因此在此咱們取個巧定義飛線的長度是其中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
  }
}

一根飛線的故事-SVG篇

上面的說明看不懂?靈魂畫手圖片解析

定義好飛線的特性變量以後,接下來咱們能夠繪製具體的飛線了。

由於飛線是若干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)
    }
  }
 }

一根飛線的故事-SVG篇

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變量由小到大變化,控制飛線由起始點到軌跡終點移動。

一根飛線的故事-SVG篇

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"屬性便可。

一根飛線的故事-SVG篇

直的搞定了,如今就是考驗咱們的時候了。咱們須要使用熟練的技巧將耿直的它給掰彎了。

下圖是一根二次貝塞爾曲線的繪製過程。由於軌跡已知,因此在各個階段的起始點都是能夠經過getPointAtLength方法得到的。惟一須要計算的只有不一樣階段貝塞爾曲線控制點的位置。能夠看到繪製它的過程當中須要持續更新控制點,爲此我去查了下二次貝塞爾曲線控制點的計算公式。

一根飛線的故事-SVG篇

一根飛線的故事-SVG篇

.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篇

這根線終於能作到從頭飛到尾了,可是尾巴有點長。這可急壞老父親了,長殘了未來可怎麼找對象啊!?

別急,畢竟他是生在我中國的一根線。線醜不怕,美顏相機來湊啊!

咱們能夠用濾鏡來先來幫它磨磨皮

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)
  })

美顏後的效果:

一根飛線的故事-SVG篇
##### 參考資料:

1. 地圖與飛線

2. 貝塞爾曲線原理

參考DEMO連接:

https://codepen.io/Narcissus_Li

更多文章推薦:

數據可視化的意義與案例分享

驚! 大屏還能長這樣!

家譜可視化案例分享

如何給數據選到合適的圖表?

相關文章
相關標籤/搜索