前端每日實戰 168# 視頻演示如何利用 Web Animation API 製作一個切換英語單詞的交互動畫

圖片描述

效果預覽

按下右側的“點擊預覽”按鈕可以在當前頁面預覽,點擊鏈接可以全屏預覽。

https://codepen.io/comehope/pen/byabeG

可交互視頻

此視頻是可以交互的,你可以隨時暫停視頻,編輯視頻中的代碼。

請用 chrome, safari, edge 打開觀看。

https://scrimba.com/p/pEgDAM/cevPbkfB

(因爲 scrimba 不支持 web animation api,所以動畫效果在視頻播放過程中看不到,不過你可以隨時暫停視頻,手工刷新預覽窗口查看動畫效果)

源代碼下載

每日前端實戰系列的全部源代碼請從 github 下載:

https://github.com/comehope/front-end-daily-challenges

代碼解讀

本作品用於展示若干包含字母組合 OO 的單詞,每點擊一下,OO 就眨眨眼,同時更換一個單詞。

整體開發過程分成 4 步,第 1 步用 CSS 實現頁面的靜態佈局,後面 3 步用 JS 實現動畫和業務邏輯。第 2 步實現單詞中間字母 OO 的眨眼效果,第 3 步實現隨機取單詞的邏輯,第 4 步實現字符的切換動畫。

眨眼動畫和字符切換動畫都是用 Web Animation API 實現的。雖然用 JS 寫動畫比用 CSS 要麻煩一些,但 API 提供了一些事件 handler,在字符切換動畫中就是利用事件機制來精確控制動畫和在動畫過程中加入業務邏輯的。

下面開始編碼。

一、靜態佈局:dom,css

dom 結構很簡單,一個名爲 .word<p> 元素中包含了 4 個 <span> 子元素,每個子元素容納一個字符:

<p class="word">
    <span>b</span>
    <span>o</span>
    <span>o</span>
    <span>k</span>
</p>

令頁面中的元素居中,設置頁面背景色爲青藍色:

body {
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: steelblue;
}

設置單詞的樣式,麻布色,大字號,大寫:

.word {
    font-size: 100px;
    color: linen;
    font-family: monospace;
    font-weight: bold;
    display: flex;
    text-transform: uppercase;
    cursor: pointer;
    user-select: none;
}

讓單詞兩端的 2 個字符變爲粉色:

.word span:first-child,
.word span:last-child {
    color: pink;
}

用徑向漸變給單詞中間的 OO 加上眼珠:

.word span:not(:first-child):not(:last-child) {
    background-image: radial-gradient(
        circle at center,
        linen 0.05em,
        transparent 0.05em
    );
}

至此,靜態佈局完成。

二、眨眼動畫

.word 元素創建一個單擊事件函數,每當點擊發生時,就先讓中間的 OO 眨眼,然後獲得下一個要顯示的單詞,再把當前的單詞換成新的單詞:

document.querySelector('.word').onclick = function() {
    //第1步:眨眼動畫
    //第2步:獲得下一個單詞
    //第3步:字符切換動畫
}

先來實現第1步-眨眼動畫。在此之前瞭解一下 Web Animation API 的語法,下面是一個簡單的示例:

let keyframes = [
    {transform: 'scaleY(1)'},
    {transform: 'scaleY(0.1)'},
]
let options = {
    duration: 200,
    iterations: 2,
}
element.animate(keyframes, options)

animate() 方法接收 2 個參數,第 1 個參數是一個數組,用於定義關鍵幀;第 2 個參數是一個對象,用於定義動畫屬性,它們分別對應着 CSS 中的 @keyframes 語句和 animation 屬性。上面的 JS 代碼等價於以下 CSS 代碼:

@keyframes anim {
    from {
        transform: scaleY(1);
    }

    to {
        transform: scaleY(0);
    }
}

.element {
    animation-name: anim;
    animation-duration: 200ms;
    animation-iteration-count: 2;
}

好了,我們來正式寫眨眼動畫:

function blinkEyes() {
    let eyes = document.querySelectorAll('.word span:not(:first-child):not(:last-child)')
    let keyframes = [
        {transform: 'scaleY(1)', offset: 0},
        {transform: 'scaleY(0.1)', offset: 0.25},
        {transform: 'scaleY(1)', offset: 0.5},
        {transform: 'scaleY(1)', offset: 1},
    ]
    let options = {
        duration: 200,
        iterations: 2,
    }
    eyes.forEach(eye => eye.animate(keyframes, options))
}

上面代碼中的 offset@keyframes 中爲每一幀指定的百分比值。這段動畫的意思是每次動畫眨眼 2 次,每次眨眼用時 200ms,這 200ms 的前 50% 時間(即前 100ms)做眨眼動作,後 50% 時間等待,這樣設計的目的是在 2 次眨眼之間插入 100ms 的間隔。

然後,在點擊事件裏調用上面的方法:

document.querySelector('.word').onclick = function() {
    //第1步:眨眼動畫
    blinkEyes()

    //第2步:獲得下一個單詞
    //第3步:字符切換動畫
}

至此,當用鼠標點擊文字時,OO 就會眨動。

三、獲得下一個單詞

接下來寫一點業務邏輯,用於隨機取出一個單詞。

引入 lodash 庫:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

定義一個名爲 Word 的類:

function Word() {
    const WORDS = ['book', 'boot', 'cook', 'cool', 'door', 'food', 'fool', 'foot', 'good', 'look', 'loop', 'moon', 'noon', 'pool', 'poor', 'room', 'roof','root', 'soon', 'tool', 'wood', 'zoom',]
    let current = 'book'
    this.getNext = () => {return current = _(WORDS).without(current).sample()}
}

Word 類有一個名爲 getNext() 的方法,用於從預設的數組中隨機取出一個單詞,可以用下面的代碼測試一下效果,會輸出類似 food 這樣的單詞:

let word = new Word()
console.log(word.getNext())

因爲接下來的動畫只涉及單詞左右兩側的字母,所以在 getNext() 方法中再把兩端的字符拆出來,返回一個對象:

function Word() {
    const WORDS = ['book', 'boot', 'cook', 'cool', 'door', 'food', 'fool', 'foot', 'good', 'look', 'loop', 'moon', 'noon', 'pool', 'poor', 'room', 'roof','root', 'soon', 'tool', 'wood', 'zoom',]
    let current = 'book'
    this.getNext = () => {
        current = _(WORDS).without(current).sample()
        return {
            first: current.slice(0, 1),
            last: current.slice(-1)
        }
    }
}

再測試一下效果,輸出結果會變爲類似 {first: "f", last: "d"} 的對象。

在點擊事件中調用上面的函數,把結果存入一個名爲 chars 的變量中:

let word = new Word()

document.querySelector('.word').onclick = function() {
    //step 1: eyes blink animation
    blinkEyes()

    //第2步:獲得下一個單詞
    let chars = word.getNext()

    //第3步:字符切換動畫
}

四、字符切換動畫

該製作字符切換動畫了。

函數的聲明如下,函數名爲 switchChar,它接收 2 個參數,第 1 個參數表示對哪個字符執行動畫,值爲 firstlast,第 2 個參數是將被替換成的新字符:

function switchChar(which, char) {}

這樣來調用:

switchChar('first', 'f')

先實現更換邏輯,不包含動畫效果:

function switchChar(which, char) {
    let letter = {
        first: {
            dom: document.querySelector('.word span:first-child'),
        },
        last: {
            dom: document.querySelector('.word span:last-child'),
        }
    }[which]

    letter.dom.textContent = char
}

在點擊事件中調用 switchChar 函數:

document.querySelector('.word').onclick = function() {
    //step 1: eyes blink animation
    blinkEyes()

    //第2步:獲得下一個單詞
    let chars = word.getNext()

    //第3步:字符切換動畫
    Object.keys(chars).forEach(key => switchChar(key, chars[key]))
}

現在運行程序的話,在每次點擊之後,單詞兩側的字符都會更新。

接下來寫動畫效果,方法和寫眨眼動畫類似。這裏有兩點要說明,一是因爲有 firstlast 2 個字符、又有入場、出場 2 個動畫,所以實際上一共實現了 4 個動畫效果;二是動畫的流程是先讓舊字符出場,再讓新字符入場,而更換字符的操作放置在這 2 個動畫中間,這是用動畫 API 的 onfinish 事件實現的:

function switchChar(which, char) {
    let letter = {
        first: {
            dom: document.querySelector('.word span:first-child'),
            to: '-0.5em',
            from: '0.8em',
        },
        last: {
            dom: document.querySelector('.word span:last-child'),
            to: '0.5em',
            from: '-0.8em',
        }
    }[which]

    let keyframes = {
        out: [
            {transform: `translateX(0)`, filter: 'opacity(1)'},
            {transform: `translateX(${letter.to})`, filter: 'opacity(0)'},
        ],
        in: [
            {transform: `translateX(${letter.from})`, filter: 'opacity(0)'},
            {transform: `translateX(0)`, filter: 'opacity(1)'},
        ]
    }

    let options = {
        duration: 500,
        fill: 'forwards',
        easing: 'cubic-bezier(0.5, 1.5, 0.5, 1.5)'
    }

    letter.dom
        .animate(keyframes.out, options)
        .onfinish = function() {
            letter.dom.animate(keyframes.in, options)
            letter.dom.textContent = char
        }
}

至此,全部編碼完成。解讀 JS 代碼和解讀 CSS 代碼不一樣,因爲不是每一行代碼都有視覺效果,很難用語言描述。如果你有不理解的地方,一定是我沒有講清楚,那麼請你多看幾遍視頻,仔細體會。

在前端每日實戰的第 162 號作品中也曾使用過 Web Animation API,但那個作品的業務邏輯比這個要複雜,你在理解了這個作品之後若還想再挑戰一下,可以再去參考它。

大功告成!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章