如何利用 Web Animation API 制作一個(gè)切換英語單詞的交互動(dòng)畫
效果預(yù)覽
按下右側(cè)的“點(diǎn)擊預(yù)覽”按鈕可以在當(dāng)前頁面預(yù)覽,點(diǎn)擊鏈接可以全屏預(yù)覽。
https://codepen.io/comehope/pen/byabeG
源代碼下載
https://github.com/comehope/front-end-daily-challenges
代碼解讀
本作品用于展示若干包含字母組合 OO 的單詞,每點(diǎn)擊一下,OO 就眨眨眼,同時(shí)更換一個(gè)單詞。
整體開發(fā)過程分成 4 步,第 1 步用 CSS 實(shí)現(xiàn)頁面的靜態(tài)布局,后面 3 步用 JS 實(shí)現(xiàn)動(dòng)畫和業(yè)務(wù)邏輯。第 2 步實(shí)現(xiàn)單詞中間字母 OO 的眨眼效果,第 3 步實(shí)現(xiàn)隨機(jī)取單詞的邏輯,第 4 步實(shí)現(xiàn)字符的切換動(dòng)畫。
眨眼動(dòng)畫和字符切換動(dòng)畫都是用 Web Animation API 實(shí)現(xiàn)的。雖然用 JS 寫動(dòng)畫比用 CSS 要麻煩一些,但 API 提供了一些事件 handler,在字符切換動(dòng)畫中就是利用事件機(jī)制來精確控制動(dòng)畫和在動(dòng)畫過程中加入業(yè)務(wù)邏輯的。
下面開始編碼。
一、靜態(tài)布局:dom,css
dom 結(jié)構(gòu)很簡單,一個(gè)名為 .word 的
元素中包含了 4 個(gè) 子元素,每個(gè)子元素容納一個(gè)字符:
<p class="word">
<span>b</span>
<span>o</span>
<span>o</span>
<span>k</span>
</p>
令頁面中的元素居中,設(shè)置頁面背景色為青藍(lán)色:
body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: steelblue;
}
設(shè)置單詞的樣式,麻布色,大字號(hào),大寫:
.word {
font-size: 100px;
color: linen;
font-family: monospace;
font-weight: bold;
display: flex;
text-transform: uppercase;
cursor: pointer;
user-select: none;
}
讓單詞兩端的 2 個(gè)字符變?yōu)榉凵?/p>
.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
);
}
至此,靜態(tài)布局完成。
二、眨眼動(dòng)畫
為 .word 元素創(chuàng)建一個(gè)單擊事件函數(shù),每當(dāng)點(diǎn)擊發(fā)生時(shí),就先讓中間的 OO 眨眼,然后獲得下一個(gè)要顯示的單詞,再把當(dāng)前的單詞換成新的單詞:
document.querySelector('.word').onclick = function() {
//第1步:眨眼動(dòng)畫
//第2步:獲得下一個(gè)單詞
//第3步:字符切換動(dòng)畫
}
先來實(shí)現(xiàn)第1步-眨眼動(dòng)畫。在此之前了解一下 Web Animation API 的語法,下面是一個(gè)簡單的示例:
let keyframes = [
{transform: 'scaleY(1)'},
{transform: 'scaleY(0.1)'},
]
let options = {
duration: 200,
iterations: 2,
}
element.animate(keyframes, options)
animate() 方法接收 2 個(gè)參數(shù),第 1 個(gè)參數(shù)是一個(gè)數(shù)組,用于定義關(guān)鍵幀;第 2 個(gè)參數(shù)是一個(gè)對(duì)象,用于定義動(dòng)畫屬性,它們分別對(duì)應(yīng)著 CSS 中的 @keyframes 語句和 animation 屬性。上面的 JS 代碼等價(jià)于以下 CSS 代碼:
@keyframes anim {
from {
transform: scaleY(1);
}
to {
transform: scaleY(0);
}
}
.element {
animation-name: anim;
animation-duration: 200ms;
animation-iteration-count: 2;
}
好了,我們來正式寫眨眼動(dòng)畫:
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 中為每一幀指定的百分比值。這段動(dòng)畫的意思是每次動(dòng)畫眨眼 2 次,每次眨眼用時(shí) 200ms,這 200ms 的前 50% 時(shí)間(即前 100ms)做眨眼動(dòng)作,后 50% 時(shí)間等待,這樣設(shè)計(jì)的目的是在 2 次眨眼之間插入 100ms 的間隔。
然后,在點(diǎn)擊事件里調(diào)用上面的方法:
document.querySelector('.word').onclick = function() {
//第1步:眨眼動(dòng)畫
blinkEyes()
//第2步:獲得下一個(gè)單詞
//第3步:字符切換動(dòng)畫
}
至此,當(dāng)用鼠標(biāo)點(diǎn)擊文字時(shí),OO 就會(huì)眨動(dòng)。
三、獲得下一個(gè)單詞
接下來寫一點(diǎn)業(yè)務(wù)邏輯,用于隨機(jī)取出一個(gè)單詞。
引入 lodash 庫:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
定義一個(gè)名為 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 類有一個(gè)名為 getNext() 的方法,用于從預(yù)設(shè)的數(shù)組中隨機(jī)取出一個(gè)單詞,可以用下面的代碼測試一下效果,會(huì)輸出類似 food 這樣的單詞:
let word = new Word()
console.log(word.getNext())
因?yàn)榻酉聛淼膭?dòng)畫只涉及單詞左右兩側(cè)的字母,所以在 getNext() 方法中再把兩端的字符拆出來,返回一個(gè)對(duì)象:
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)
}
}
}
再測試一下效果,輸出結(jié)果會(huì)變?yōu)轭愃?{first: "f", last: "d"} 的對(duì)象。
在點(diǎn)擊事件中調(diào)用上面的函數(shù),把結(jié)果存入一個(gè)名為 chars 的變量中:
let word = new Word()
document.querySelector('.word').onclick = function() {
//step 1: eyes blink animation
blinkEyes()
//第2步:獲得下一個(gè)單詞
let chars = word.getNext()
//第3步:字符切換動(dòng)畫
}
四、字符切換動(dòng)畫
該制作字符切換動(dòng)畫了。
函數(shù)的聲明如下,函數(shù)名為 switchChar,它接收 2 個(gè)參數(shù),第 1 個(gè)參數(shù)表示對(duì)哪個(gè)字符執(zhí)行動(dòng)畫,值為 first 或 last,第 2 個(gè)參數(shù)是將被替換成的新字符:
function switchChar(which, char) {}
這樣來調(diào)用:
switchChar('first', 'f')
先實(shí)現(xiàn)更換邏輯,不包含動(dòng)畫效果:
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
}
在點(diǎn)擊事件中調(diào)用 switchChar 函數(shù):
document.querySelector('.word').onclick = function() {
//step 1: eyes blink animation
blinkEyes()
//第2步:獲得下一個(gè)單詞
let chars = word.getNext()
//第3步:字符切換動(dòng)畫
Object.keys(chars).forEach(key => switchChar(key, chars[key]))
}
現(xiàn)在運(yùn)行程序的話,在每次點(diǎn)擊之后,單詞兩側(cè)的字符都會(huì)更新。
接下來寫動(dòng)畫效果,方法和寫眨眼動(dòng)畫類似。這里有兩點(diǎn)要說明,一是因?yàn)橛?first、last 2 個(gè)字符、又有入場、出場 2 個(gè)動(dòng)畫,所以實(shí)際上一共實(shí)現(xiàn)了 4 個(gè)動(dòng)畫效果;二是動(dòng)畫的流程是先讓舊字符出場,再讓新字符入場,而更換字符的操作放置在這 2 個(gè)動(dòng)畫中間,這是用動(dòng)畫 API 的 onfinish 事件實(shí)現(xiàn)的:
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 代碼不一樣,因?yàn)椴皇敲恳恍写a都有視覺效果,很難用語言描述。如果你有不理解的地方,一定是我沒有講清楚,那么請你多看幾遍視頻,仔細(xì)體會(huì)。
大功告成!
“作者:comehope 原文:https://scrimba.com/p/pEgDAM/cevPbkfB
”




