首頁 > 軟體

js基於div絲滑實現貝塞爾曲線

2022-09-24 14:01:38

引言

今天遇到朋友發來的一個ui圖,詢問我如何實現下圖這樣的效果【vue專案】,(聽說是類似LED燈的展示效果),於是便有了今天的小demo,要實現一個類似下圖的動效,上面的燈會一直重複捲動,但是這個並不是什麼難點,主要在於如何實現這種平滑的曲線,日常我們的開發的div在我們的腦海中通常就是一個網格狀,涉及到平滑曲線的往往是圖表,於是我們需要找一個方案來完成這種佈局(非真實ui圖,是完成之後的效果)

分析

我們需要先簡單分析一下這個ui,當我們拿到這個UI圖的時候,腦海中的第一反應是,一個大的DIV中間套了很多的小的DIV,並且小的的上下位置出現了偏移,但是偏移多少目前我們不得而知,但是基礎的佈局方案已經完成。

第二步我們考慮球體的顏色,可以看到,軌道是一種顏色,需要一直移動的球體是另一種顏色,這個非常簡單,我們定義兩組資料,一組是軌道,一組是高亮的球,通過不段改變高亮的這組資料,即可響應式的完成燈的移動,第二點我們也解決了

第三點,初始的時候考慮的是y的座標是0, 2, 4, 6, 8, 10 , 8, 6, 4 , 2 ..... ,但是很顯然,這樣的座標出來的形狀一定是一個折線圖,而不是平滑的曲線,於是我們需要用到數學知識了:需要使用到圓的弧度的概念,在javascript中有兩個方法**Math.sin()和Math.cos()**都是關於弧度的公式,關於這兩個方法,我們下面再說。

實現

佈局

實現這個的佈局非常簡單,外層一個大的div,內層很多小的span,通過flex一排即可到一排

<template>
  <div class="container">
    <div class="content">
      <span class="circle" v-for="(item,index) in list" :key="index"></span>
    </div>
  </div>
</template>

如何計算y的偏移量

這一步是我們比較重要的一步,我們有一個400px的容器,容器中放置了20個span,現在他們在一排,我們只需要給他動態繫結樣式**transform: translateY(?px)**即可,重要的是我們如何計算這個的座標,我們先來了解下兩個方法的用處:

Math.sin() 和 Math.cos()

Math.sin(x)      x 的正玄值。返回值在 -1.0 到 1.0 之間;

Math.cos(x)    x 的餘弦值。返回的是 -1.0 到 1.0 之間;

可以看到其分別是x點的正弦,這兩個函數中的x指的是弧度而不是角度弧度的計算公式是:2π/360°

這裡涉及到數學知識,我們先看看這張圖

我們看我們關注的sin和cos

sin(∠A) = 對邊比斜邊(a / c)
cos(∠A) = 臨邊比斜邊 (b / c)

可大致瞭解一下即可,當然,我們今天所需要使用的和這個關係不大,這裡只是幫大家回顧一下高中知識(手動狗頭)。

好了這裡直接推薦一個線上網站,繪圖計算機可以直接線上偵錯各種曲線

我們看看基礎的正弦餘弦曲線

正弦曲線

餘弦曲線

我們知道圓周率(π), 1π=180°2π=360°,就是一週,所以我們只需要截圖(0-2π)一個週期的曲線即可,後續不管要什麼曲線,都在這個上面進行變換即可,通過上面對比,發現正弦曲線的起始點是(0,0),比餘弦的(0,1)更好計算,我們就直接用正弦吧,那麼我們列出已知條件:

  • 在曲線中 y = cos(x)
  • 在曲線中,曲線的寬度是
  • 在曲線中,曲線的高度最高點到最低點是2
  • 在我們的需求中,總寬度是400px
  • 在我們需求中, 共有二十個圓圈,所以我們可以算出每個球的寬度平均是20px,所以座標就是(index+1)*20
  • 現在我們知道了很多資訊,我們就可以計算出更多資訊了

計算更多資訊

我們知道曲線的寬度和我們的物理實際寬度就可以得出寬度比: 400 / 2π

這個時候我們需要通過這個比例計算出物理的x座標對應的曲線中的x座標,那麼 物理寬度/x座標 = 2π/曲線中x座標

/* 400 / x = 2π / y, 我們的x是已知的,等下自己可以拿,這樣拿到了曲線中實際的x座標 */
const z = 400 x / 400 * Math.PI*2

有個曲線中的對應x座標,通過公式我們就可以拿到其曲線中實際y座標了

/* 這樣就拿到了曲線中的y座標 */
y = Math.sin(z)

拿到了曲線中的y座標,那麼們又知道,曲線中的總高是2,通過xy的座標對比,我們可以計算出我們所需的真實的y

/* 真實寬度400/曲線寬度2π = 真實高度y/曲線中的y 通過對比得到真實的y點 */
Y = Math.sin(z) * 400 / Math.PI * 2 / 2

然後通過這樣的一個計算公式把這個y值賦值給我們的y點就可以得到這樣的曲線

完善剩餘

看起來有點意思了,這就是一個完整的2π,或者我們理解為就是曲線的一個週期,但是很明顯曲線的度數不對,我們如何調整呢,回到剛剛的那個網站之中,我們要想曲線更加平滑,只需要對sin()除以/x即可,x最大線越平,我們到剛剛的網站去自己偵錯到自己理想的高度,

我們偵錯發現除以4就得到了差不多我們想要的曲線,所以我們只需要在上面的基礎上/4就得到了我們真正想要的y。

此時我們的曲線就已經完成了,所以其實是不是就是我們的高中數學知識呢

完成跑馬燈製作

前面的曲線畫完,後面就已經不難了,我們只需要定義一段高亮的下標陣列,我們寫一個方法,建立一個自己想要高亮幾個就生成0-x的陣列

createActiveIndex(len = 6){
  return Array.from({length:len}, (v,k) => k)
},

然後在給span動態繫結一個背景顏色。當index屬於高亮的時候就給高亮的顏色,不是則反之,然後我們寫一個定時器一直修改這個高亮的陣列即可,每次讓其裡面所有元素加1,就可以讓他一直跑下去了,當然邊界的時候我們需要對他進行歸0

changeIndex(){
   this.activeIndex = this.activeIndex.map( item => item === this.list.length - 1 ? 0 : item + 1)
},

最後我們啟動即可,就實現了我們開頭想要的效果。

至此這個需求算是完成了,這只是一個小的場景通過這樣的方式我們可以繪製出更多好玩的東西,你可以改變各種引數對齊進行調整修改,看看是不是你想要的效果

貝塞爾曲線

我們知道,前端的動畫經常出現一個名詞貝塞爾曲線,就是動畫的執行過程,我們剛剛的曲線其實就是同理,如果此時我們需要去手動書寫一個貝塞爾曲線我們應該怎麼做呢,剛剛我們知道,我們容器的總寬度是400,曲線的周長是2π,比例就是400/2π,同理,當我們換算成時間的時候,假如動畫是1秒。

那麼我們需要60幀,一幀動畫的時間就是1000/60=16.7ms,我們通過2π/60就知道我們每一幀動畫在什麼位置了,當我們手寫貝塞爾曲線的時候,利用差不多的公式一樣可以完成。

簡單封裝一下方法

看起來似乎很複雜,但是實際上我們所需要的其實只是利用真實的x點,拿到對應曲線求出我們y的座標,所以我們需要的引數有,我們真實場景的總寬,總寬之中的個數,我們所需要的曲線的倍率,三個引數即可,我們儘量分開步驟寫,這樣你看會理解的更清楚

js中π就是Math.PI

function getCoordinate(width, count,  mag = 1){
      /* 通過總寬和個數計算出一個單個的寬 */
      const singleWidth = width / count
      /* 通過物理寬度/曲線周長計算出比率 */
      const ratio = 400 / Math.PI*2
      /* 上面範例程式碼我們是動態一次計算一個,而現在是方法,我們應該一次去拿到所有,所以我們返回一個陣列物件記錄xy */
      let result = new Array(count).fill({})
      /* 遍歷總長度的dom個數,在陣列中填充寬高 */
      result = result.map( (item,index) => {
          /* x的座標 */
          const x = (index + 1) * singleWidth
          /* 定義變數z計算曲線中x的座標 */
          const z = x / width * Math.PI*2
          /* 計算出真實的y的座標 */
          let y = Math.sin(z) / 4  * 400 / Math.PI * 2 / 2    
          /* y還需要通過倍率改變曲線,得到最終我們想要的y */
          y = y / mag
          /* 寫入陣列物件中 */
          return {x, y}
      })
      return result;
    } 

完整範例

style

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #000;
}
.content{
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 400px;
  height: 50px;
}
.container .circle{
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background-color: #befbf7;
}

SCript

<template>
  <div class="container">
    <div class="content">
      <span class="circle" v-for="(item,index) in list" :key="index" :style="{transform: `translateY(${calcY(index)}px)`,backgroundColor: getCurrentBgColor(index)}"></span>
    </div>
     <div class="content" style="margin-top: 50px">
      <span class="circle" v-for="(item,index) in list" :key="index" :style="{transform: `translateY(${calcY(index)}px)`,backgroundColor: getRandomBgColor(index)}"></span>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      list: [], //定義總長度
      activeIndex: [], // 運動中的球的顏色
      interval: 300, //運動速度
      colors: { // 定義軌道顏色和高亮顏色
        active: '#2b88ff',
        basic: '#b6f3f7' 
      },
      cache: []
    };
  },
  methods: {
    init() {
      this.list = new Array(20).fill(0)
      this.start()
    },
    getCurrentBgColor(index){
      return this.activeIndex.includes(index) ? '#6e9cae' : this.getRandomColor()
    },
    getRandomBgColor(index){
      const color = this.activeIndex.includes(index) ? 'active' : 'basic'
      return this.colors[color]
    },
    start(){
      this.activeIndex = this.createActiveIndex()
      setInterval(() => this.changeIndex(), this.interval)
    },
    changeIndex(){
      this.activeIndex = this.activeIndex.map( item => item === this.list.length - 1 ? 0 : item + 1)
    },
    /* 生成需要動的球的個數 */
    createActiveIndex(len = 6){
      return Array.from({length:len}, (v,k) => k)
    },
    getRandomColor(){
        return `#${Math.floor(Math.random() * 0xffffff) .toString(16)}`;
    },
    getCoordinate(width, count,  mag = 1){
      /* 通過總寬和個數計算出一個單個的寬 */
      const singleWidth = width / count
      /* 通過物理寬度/曲線周長計算出比率 */
      const ratio = 400 / Math.PI*2
      /* 上面範例程式碼我們是動態一次計算一個,而現在是方法,我們應該一次去拿到所有,所以我們返回一個陣列物件記錄xy */
      let result = new Array(count).fill({})
      /* 遍歷總長度的dom個數,在陣列中填充寬高 */
      result = result.map( (item,index) => {
          /* x的座標 */
          const x = (index + 1) * singleWidth
          /* 定義變數z計算曲線中x的座標 */
          const z = x / width * Math.PI*2
          /* 計算出真實的y的座標 */
          let y = Math.sin(z) / 4  * 400 / Math.PI * 2 / 2    
          /* y還需要通過倍率改變曲線,得到最終我們想要的y */
          y = y / mag
          /* 寫入陣列物件中 */
          return {x, y}
      })
      return result;
    } 
  },
  created(){
    this.cache = this.getCoordinate(400, 20, 1)
    this.init()
  },
  computed:{
    calcY(){
      return (index) => {
        /* 使用封裝的方法計算 */
        // return this.cache[index].y
        const x = (index + 1) * 20
        const z = x / 400 * Math.PI*2
        const y = Math.sin(z) * 400 / Math.PI * 2 / 2 / 4
        return y
      }
    }
  }
};
</script>

以上就是js基於div絲滑實現貝塞爾曲線的詳細內容,更多關於js div實現貝塞爾曲線的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com