js基于div絲滑實現(xiàn)貝塞爾曲線
引言
今天遇到朋友發(fā)來的一個ui圖,詢問我如何實現(xiàn)下圖這樣的效果【vue項目】,(聽說是類似LED燈的展示效果),于是便有了今天的小demo,要實現(xiàn)一個類似下圖的動效,上面的燈會一直重復(fù)滾動,但是這個并不是什么難點,主要在于如何實現(xiàn)這種平滑的曲線,日常我們的開發(fā)的div在我們的腦海中通常就是一個網(wǎng)格狀,涉及到平滑曲線的往往是圖表,于是我們需要找一個方案來完成這種布局(非真實ui圖,是完成之后的效果)

分析
我們需要先簡單分析一下這個ui,當我們拿到這個UI圖的時候,腦海中的第一反應(yīng)是,一個大的DIV中間套了很多的小的DIV,并且小的的上下位置出現(xiàn)了偏移,但是偏移多少目前我們不得而知,但是基礎(chǔ)的布局方案已經(jīng)完成。
第二步我們考慮球體的顏色,可以看到,軌道是一種顏色,需要一直移動的球體是另一種顏色,這個非常簡單,我們定義兩組數(shù)據(jù),一組是軌道,一組是高亮的球,通過不段改變高亮的這組數(shù)據(jù),即可響應(yīng)式的完成燈的移動,第二點我們也解決了
第三點,初始的時候考慮的是y的坐標是0, 2, 4, 6, 8, 10 , 8, 6, 4 , 2 ..... ,但是很顯然,這樣的坐標出來的形狀一定是一個折線圖,而不是平滑的曲線,于是我們需要用到數(shù)學知識了:需要使用到圓的弧度的概念,在javascript中有兩個方法**Math.sin()和Math.cos()**都是關(guān)于弧度的公式,關(guān)于這兩個方法,我們下面再說。
實現(xiàn)
布局
實現(xiàn)這個的布局非常簡單,外層一個大的div,內(nèi)層很多小的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,現(xiàn)在他們在一排,我們只需要給他動態(tài)綁定樣式**transform: translateY(?px)**即可,重要的是我們?nèi)绾斡嬎氵@個的坐標,我們先來了解下兩個方法的用處:
Math.sin() 和 Math.cos()
Math.sin(x) x 的正玄值。返回值在 -1.0 到 1.0 之間;
Math.cos(x) x 的余弦值。返回的是 -1.0 到 1.0 之間;
可以看到其分別是x點的正弦,這兩個函數(shù)中的x指的是弧度而不是角度,弧度的計算公式是:2π/360°
這里涉及到數(shù)學知識,我們先看看這張圖

我們看我們關(guān)注的sin和cos
sin(∠A) = 對邊比斜邊(a / c)
cos(∠A) = 臨邊比斜邊 (b / c)
可大致了解一下即可,當然,我們今天所需要使用的和這個關(guān)系不大,這里只是幫大家回顧一下高中知識(手動狗頭)。
好了這里直接推薦一個在線網(wǎng)站,圖形計算器可以直接在線調(diào)試各種曲線
我們看看基礎(chǔ)的正弦余弦曲線
正弦曲線

余弦曲線

我們知道圓周率(π), 1π=180°,2π=360°,就是一周,所以我們只需要截圖(0-2π)一個周期的曲線即可,后續(xù)不管要什么曲線,都在這個上面進行變換即可,通過上面對比,發(fā)現(xiàn)正弦曲線的起始點是(0,0),比余弦的(0,1)更好計算,我們就直接用正弦吧,那么我們列出已知條件:
- 在曲線中 y = cos(x)
- 在曲線中,曲線的寬度是2π
- 在曲線中,曲線的高度最高點到最低點是2
- 在我們的需求中,總寬度是400px
- 在我們需求中, 共有二十個圓圈,所以我們可以算出每個球的寬度平均是20px,所以坐標就是
(index+1)*20 - 現(xiàn)在我們知道了很多信息,我們就可以計算出更多信息了
計算更多信息
我們知道曲線的寬度和我們的物理實際寬度就可以得出寬度比: 400 / 2π
這個時候我們需要通過這個比例計算出物理的x坐標對應(yīng)的曲線中的x坐標,那么 物理寬度/x坐標 = 2π/曲線中x坐標
/* 400 / x = 2π / y, 我們的x是已知的,等下自己可以拿,這樣拿到了曲線中實際的x坐標 */ const z = 400 x / 400 * Math.PI*2
有個曲線中的對應(yīng)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π,或者我們理解為就是曲線的一個周期,但是很明顯曲線的度數(shù)不對,我們?nèi)绾握{(diào)整呢,回到剛剛的那個網(wǎng)站之中,我們要想曲線更加平滑,只需要對sin()除以/x即可,x最大線越平,我們到剛剛的網(wǎng)站去自己調(diào)試到自己理想的高度,

我們調(diào)試發(fā)現(xiàn)除以4就得到了差不多我們想要的曲線,所以我們只需要在上面的基礎(chǔ)上/4就得到了我們真正想要的y。

此時我們的曲線就已經(jīng)完成了,所以其實是不是就是我們的高中數(shù)學知識呢
完成跑馬燈制作
前面的曲線畫完,后面就已經(jīng)不難了,我們只需要定義一段高亮的下標數(shù)組,我們寫一個方法,創(chuàng)建一個自己想要高亮幾個就生成0-x的數(shù)組
createActiveIndex(len = 6){
return Array.from({length:len}, (v,k) => k)
},
然后在給span動態(tài)綁定一個背景顏色。當index屬于高亮的時候就給高亮的顏色,不是則反之,然后我們寫一個定時器一直修改這個高亮的數(shù)組即可,每次讓其里面所有元素加1,就可以讓他一直跑下去了,當然邊界的時候我們需要對他進行歸0
changeIndex(){
this.activeIndex = this.activeIndex.map( item => item === this.list.length - 1 ? 0 : item + 1)
},
最后我們啟動即可,就實現(xiàn)了我們開頭想要的效果。

至此這個需求算是完成了,這只是一個小的場景通過這樣的方式我們可以繪制出更多好玩的東西,你可以改變各種參數(shù)對齊進行調(diào)整修改,看看是不是你想要的效果
貝塞爾曲線
我們知道,前端的動畫經(jīng)常出現(xiàn)一個名詞貝塞爾曲線,就是動畫的執(zhí)行過程,我們剛剛的曲線其實就是同理,如果此時我們需要去手動書寫一個貝塞爾曲線我們應(yīng)該怎么做呢,剛剛我們知道,我們?nèi)萜鞯目倢挾仁?00,曲線的周長是2π,比例就是400/2π,同理,當我們換算成時間的時候,假如動畫是1秒。
那么我們需要60幀,一幀動畫的時間就是1000/60=16.7ms,我們通過2π/60就知道我們每一幀動畫在什么位置了,當我們手寫貝塞爾曲線的時候,利用差不多的公式一樣可以完成。
簡單封裝一下方法
看起來似乎很復(fù)雜,但是實際上我們所需要的其實只是利用真實的x點,拿到對應(yīng)曲線求出我們y的坐標,所以我們需要的參數(shù)有,我們真實場景的總寬,總寬之中的個數(shù),我們所需要的曲線的倍率,三個參數(shù)即可,我們盡量分開步驟寫,這樣你看會理解的更清楚
js中π就是Math.PI
function getCoordinate(width, count, mag = 1){
/* 通過總寬和個數(shù)計算出一個單個的寬 */
const singleWidth = width / count
/* 通過物理寬度/曲線周長計算出比率 */
const ratio = 400 / Math.PI*2
/* 上面實例代碼我們是動態(tài)一次計算一個,而現(xiàn)在是方法,我們應(yīng)該一次去拿到所有,所以我們返回一個數(shù)組對象記錄xy */
let result = new Array(count).fill({})
/* 遍歷總長度的dom個數(shù),在數(shù)組中填充寬高 */
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
/* 寫入數(shù)組對象中 */
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)
},
/* 生成需要動的球的個數(shù) */
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){
/* 通過總寬和個數(shù)計算出一個單個的寬 */
const singleWidth = width / count
/* 通過物理寬度/曲線周長計算出比率 */
const ratio = 400 / Math.PI*2
/* 上面實例代碼我們是動態(tài)一次計算一個,而現(xiàn)在是方法,我們應(yīng)該一次去拿到所有,所以我們返回一個數(shù)組對象記錄xy */
let result = new Array(count).fill({})
/* 遍歷總長度的dom個數(shù),在數(shù)組中填充寬高 */
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
/* 寫入數(shù)組對象中 */
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絲滑實現(xiàn)貝塞爾曲線的詳細內(nèi)容,更多關(guān)于js div實現(xiàn)貝塞爾曲線的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Servlet3.0與純javascript通過Ajax交互的實例詳解
Servlet與純javascript通過Ajax交互,對于很多人來說應(yīng)該很簡單。不過還是寫寫,方便Ajax學習的后來者2018-03-03
微信小程序開發(fā)一鍵登錄 獲取session_key和openid實例
這篇文章主要介紹了微信小程序開發(fā)一鍵登錄 獲取session_key和openid實例的相關(guān)資料,需要的朋友可以參考下2016-11-11
JS前端宏任務(wù)微任務(wù)及Event Loop使用詳解
這篇文章主要為大家介紹了JS前端宏任務(wù)微任務(wù)及Event Loop使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07
微信小程序 開發(fā)之滑塊視圖容器(swiper)詳解及實例代碼
這篇文章主要介紹了微信小程序 開發(fā)之滑塊視圖容器(swiper)詳解及實例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02
ant-design的upload組件中實現(xiàn)粘貼上傳實例詳解
這篇文章主要為大家介紹了ant-design的upload組件中實現(xiàn)粘貼上傳實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05
JS版的date函數(shù)(和PHP的date函數(shù)一樣)
這篇文章主要介紹了JS版的date函數(shù),使用方法和PHP的date函數(shù)一樣,需要的朋友可以參考下2014-05-05

