【不可思議的前端】動如脫兔的小球

D文斌發表於2019-12-26

本篇分享給大家一個有意思的小案例,同時我也會手摸手地教大家這個小案例是如何實現的,最後做一個簡短2019年總結和2020年的展望,畢竟人還活著呢。

【不可思議的前端】動如脫兔的小球

看到這個動圖不知道是否勾起了你的好奇心,如果你心想這是如何來實現的呢?那麼請不要著急,接下來我會帶你來實現它。再如果你只對動圖裡的字型感興趣,同樣也不要著急,文章底部已經貼好了原始碼連結,字型庫就在其中。如果你想了解如何實現它的話,接下來就跟隨我一探究竟。

分析

當讓你根據動圖所呈現的效果來實現它的話,我們的第一個想法會是什麼?去 Google ...(sleep 3s)顯然在這不現實。 那我們來換一種想法,先看它都有哪些元素。

從動圖來看它有 文字小球動畫 這三種重要元素。接下來我們只需要一一實現它就可以了。

每當我們實現一個需求或者完成一項任務的時候,首先最最需要的是抽象和結構化思維,在腦海中有一個大概的構思,實現思路大致如下:

  1. 插入文字並計算好文字距離左邊的距離
  2. 根據距離計算出小球的每個落點
  3. 新增動畫改變各自的狀態

實現

第一步

首先我們什麼都不用思考,上來先定義一個 class 名字叫做 BounceBall

class BounceBall {}
複製程式碼

我們打算當使用者建立一個例項時,傳給我們一段文字和一個繫結在 dom 上的 id,我們就可以把文字插入在這個 dom 內。這樣會在 html 中呈現類似下面的效果

<div>Goodbye 2019 Hello 2020</div>
複製程式碼

問題出現了,當一段文字只放在一個元素內的話,我們無法計算出每個文字距離左邊的距離,這時候我們就需要做一些改變。

根據空格切分字串成為陣列,將每一個文字插入單獨的元素內,同樣中間用空格分開。

class BounceBall {
    constructor (config) {
        const { id, text } = config
        this.id = id
        this.text = text
        this.$id = document.getElementById(this.id)
        this.init()
    }
    
    init () {
        const contentArray = this.text.split(' ')
        // this.append(this.$ball)
        for (let i = 0, len = contentArray.length; i < len; i++) {
            const text = contentArray[i]

            const $text = getSpan(text, 'text')
            this.append($text)

            const textLen = $text.offsetWidth

            if (i + 1 < contentArray.length) {
                this.append(getSpan(' '))
            }
        }
    }
    
    append (element) {
        this.$id.appendChild(element)
    }
}
複製程式碼

這裡有一個 getSpan 方法沒有在上面程式碼中體現出來,它的目的是建立一個 span 標籤元素,然後把字串插入進去,新增 classname 等。 因為我沒有藉助任何庫,所以很多 dom 操作需要單獨封裝成工具庫。再看一下 html 中呈現的效果

<div id="main">
    <!-- <div class="ball"></div> -->
    <span class="text">Goodbye</span>
    <span> </span>
    <span class="text">2019</span>
    <span> </span>
    <span class="text">Hello</span>
    <span> </span>
    <span class="text">2020</span>
</div>
複製程式碼

這樣我們就可以根據 classname 獲得每個文字的屬性了。接下來小球的 dom 同樣一併新增上,小球為上面註釋部分。

給小球新增樣式讓它在文字的左上角位置。

.ball {
  position: absolute;
  top: 0;
  left: -20px;
  width: 10px;
  height: 10px;
  border-radius: 100%;
  background-color: green;
  margin-left: -5px;
}
複製程式碼

【不可思議的前端】動如脫兔的小球

第二步

計算出每段文字距離左邊的距離寬度位置

init () {
    ...
    
    for (let i = 0, len = contentArray.length; i < len; i++) {
        const text = contentArray[i]

        const $text = getSpan(text, 'text')
        this.append($text)
        
        const textLen = $text.offsetWidth
        ...
        
        const ballLeft = $text.offsetLeft + textLen / 2
    
        const ballProps: BallProps = {
            left: ballLeft,
            textLen,
            textIndex: i
        }
        this.ballPropsArray.push(ballProps)
    }
}
複製程式碼

我們定義了一個 ballPropsArray 陣列,它存放著小球運動的軌跡。ballLeft 為每段文字中間距離左邊的距離,也就是小球要到的位置。textLen 為每段文字的寬度。textIndex 為下標。

我們假設小球移動到每個 ballProps 的時間為 ${textLen} ms。當 ${textLen / 2} ms時,小球距離左邊為 ${ballLeft} px,高度設定一個定值,再過 ${textLen / 2} ms 小球下落到底部。根據這個思路我們實現出以下程式碼:


let incrementingDelay = 0

for (let i = 0, len = this.ballPropsArray.length; i < len; i++) {
  const ballProps = this.ballPropsArray[i]
  setTimeout(() => {
    this.$ball.style.left = `${ballProps.left}px`
    this.$ball.style.top = '-1em'

    // 小球開始上升
    const halfwayReached = ballProps.textLen / 2
    setTimeout(() => {
      this.$ball.style.left = `${ballProps.left}px`
      this.$ball.style.top = '0px'
      
      // 小球開始下落
    }, halfwayReached)
  }, incrementingDelay)

  incrementingDelay += ballProps.textLen
}
複製程式碼

【不可思議的前端】動如脫兔的小球

第三步

為小球新增動畫改變文字的顏色,首先給小球新增 transition 屬性。

.ball {
  ...
  transition-property: left, top;
  transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1), cubic-bezier(0.25, 0.1, 0.25, 1);
}
複製程式碼

在每次運動中設定 transition-duration 的值。

const leftDuration = `${ballProps.textLen}ms`
const topDuration = `${ballProps.textLen / 2}ms`

this.$ball.style.transitionDuration = `${leftDuration}, ${topDuration}`
複製程式碼

在小球開始下落的 ${textLen / 2} ms 之後,修改文字的顏色。

let incrementingDelay = 0

for (let i = 0, len = this.ballPropsArray.length; i < len; i++) {
  const ballProps = this.ballPropsArray[i]
  setTimeout(() => {
    this.$ball.style.left = `${ballProps.left}px`
    this.$ball.style.top = '-1em'

    // 小球開始上升
    const halfwayReached = ballProps.textLen / 2
    setTimeout(() => {
      this.$ball.style.left = `${ballProps.left}px`
      this.$ball.style.top = '0px'
      
      // 小球開始下落
      setTimeout(() => {
        // 修改顏色
      }, halfwayReached)
    }, halfwayReached)
  }, incrementingDelay)

  incrementingDelay += ballProps.textLen
}
複製程式碼

【不可思議的前端】動如脫兔的小球

一個簡版的跳動小球就做好了,之後我們可以繼續為它新增更多的屬性和進一步優化,比如 為它新增 speed 屬性,或者讓小球淡入淡出,或者改變它的形狀等等。

結語

2019年馬上要過去了,藉著這篇文章做個年終總結。今年是我收穫之年,有很多朋友、同事和親戚,不管在工作中還是生活中都給我很大幫助。年初為自己制定的一些學習計劃和工作計劃也都完成的不錯,比如每個月讀一本書,每個月發一篇文章等。工作中要學一些新技術、做一些對團隊有幫助意義的事情等。希望在2020年自己依舊能堅持下去,嘗試更多新領域、探索更多未知。畢竟人活著總要給這個世界留下的什麼吧。最後祝大家2020年一夜暴富。

附上小球原始碼 bounce-ball

相關文章