最近的一個(gè)頁面需要一個(gè)文字水平滾動(dòng)的效果,效果如圖所示:

本來想在網(wǎng)上直接找代碼一把梭,但發(fā)現(xiàn)現(xiàn)有代碼都是用的 jQuery + animation,不符合項(xiàng)目的技術(shù)棧,就想干脆自己實(shí)現(xiàn)一個(gè)。最終用了18行代碼實(shí)現(xiàn)了上面的效果。
原理
具體原理跟輪播圖類似,把包裹文字的元素復(fù)制多一個(gè)出來,當(dāng)?shù)谝粋€(gè)元素滾動(dòng)到看不見的時(shí)候,把第一個(gè)元素重新插到尾部,因?yàn)閮蓚€(gè)元素是緊挨著的,第一個(gè)元素滾動(dòng)到看不見的一瞬間,第二個(gè)元素也已經(jīng)到了開始的位置,所以肉眼看不出來元素插入的過程。最終實(shí)現(xiàn)了滾動(dòng)效果。
實(shí)現(xiàn)
HTML
<div class="scrollX">
<p class="scrollTxt">窗前明月光,疑是地上霜,舉頭望明月,低頭思故鄉(xiāng)</p>
</div>
第一步,拿到<p>標(biāo)簽并復(fù)制,插入到父容器中:
const domScroll = document.querySelector('.scrollTxt');
const cloneNode = document.querySelector('.scrollTxt').cloneNode(true);
domScroll.parentNode.appendChild(cloneNode);
此時(shí)的HTML標(biāo)簽就變成了:
<div class="scrollX">
<p class="scrollTxt">窗前明月光,疑是地上霜,舉頭望明月,低頭思故鄉(xiāng)</p>
<p class="scrollTxt">窗前明月光,疑是地上霜,舉頭望明月,低頭思故鄉(xiāng)</p>
</div>
這樣有兩個(gè)問題:
①父元素隨著子元素的增大而增大,需要給父元素一個(gè)固定的寬度,且隱藏溢出的部分;
②兩個(gè)<p>標(biāo)簽不在同一行,需要讓兩個(gè)元素在同一行才有水平滾動(dòng)的效果。
針對(duì)第一個(gè)問題,給 scrollX一個(gè)固定寬度加 overflow: hidden就可以解決。
第二個(gè)問題,我是用了 flex布局,在父元素定義 display: flex就讓兩個(gè)子元素在了同一個(gè)水平線上。最終的CSS:
.scrollX {
display: flex;
height: 100%;
width: 500px;
overflow: hidden;
p {
height: 100%;
lex-shrink: 0;
padding-right: 20px;
white-space: nowrap;
}
}
讓元素動(dòng)起來
讓元素滾動(dòng)有兩種方法
①將元素設(shè)置為 position: absolute并控制元素的left屬性,讓元素往左移動(dòng)。
②使用transform并控制translateX()讓元素往左移動(dòng)。
沒吃過豬肉也見過豬跑,沒寫過動(dòng)畫的人,也知道第二種方法的性能會(huì)比第一種要好。為什么好呢,我們放在最后面總結(jié),先把動(dòng)畫實(shí)現(xiàn)起來~
我們要讓元素往左滾,那么要滾多遠(yuǎn)呢?就是元素本身的寬度,當(dāng)元素滾動(dòng)的距離超過自身寬度時(shí),進(jìn)行元素插入尾部的操作:
const domScroll2 = document.querySelectorAll('.scrollTxt')[1];
let width = domScroll2.getAttribute('width');
const animation = () => {
if (width > -100) {
domScroll.style = `transform: translateX(${width}%)`;
domScroll2.style = `transform: translateX(${width}%)`;
width -= 1;
} else {
domScroll.parentNode.appendChild(domScroll);
width = 0;
}
setTimeout(animation, 100); // 使用setTimeout 嵌套代替 setInterval
};
animation();
代碼寫到這里,動(dòng)畫就可以動(dòng)起來了。
這里有一個(gè)實(shí)現(xiàn)的細(xì)節(jié)是,用 setTimeout 的嵌套代替setInterval,原因是 setInterval 的運(yùn)行機(jī)制并不是真實(shí)的“每隔XX秒后調(diào)用指定的函數(shù)”,因?yàn)?strong>setInterval并不會(huì)將指定函數(shù)需要執(zhí)行的時(shí)間考慮在內(nèi)。比如我們用setInterval每隔100ms調(diào)用一次函數(shù)A,如果A本身的執(zhí)行需要95ms,那么A執(zhí)行完,經(jīng)過至少5ms就會(huì)馬上被調(diào)用,所以為了保證每次執(zhí)行都有一定的間隔,要使用 setTimeout代替setInterval。
在查資料的過程中,想起來還有requestAnimationFrame這個(gè)API,這個(gè)API相當(dāng)于一個(gè)時(shí)間被固定為 瀏覽器繪制頻率(一般為16ms一次) 的setTimeout,但它是setTimeout的改進(jìn)版,瀏覽器會(huì)為requestAnimationFrame做優(yōu)化,以每一次瀏覽器的重繪為間隔執(zhí)行動(dòng)畫。而且,當(dāng)瀏覽器不在當(dāng)前 tab 的時(shí)候,requestAnimationFrame調(diào)用的動(dòng)畫會(huì)停止。會(huì)根據(jù)瀏覽器繪制頻率調(diào)整刷新時(shí)機(jī)。
GPU渲染導(dǎo)致的動(dòng)畫中字體模糊的問題
一開始實(shí)現(xiàn)的時(shí)候考慮過用 transition 來實(shí)現(xiàn)運(yùn)動(dòng)效果,發(fā)現(xiàn)運(yùn)動(dòng)過程中字體會(huì)變得模糊,而且不僅是動(dòng)畫中的字體會(huì)模糊,連頁面上的其他字體也變得模糊起來。在實(shí)現(xiàn)了動(dòng)畫后考慮過使用will-change優(yōu)化動(dòng)畫,發(fā)現(xiàn)也存在同樣的問題(區(qū)別是will-change只會(huì)導(dǎo)致運(yùn)動(dòng)的字體變模糊)。
一番查找發(fā)現(xiàn)文字模糊是因?yàn)闉g覽器啟用了GPU渲染,而GPU渲染不具備CPU渲染的次像素抗鋸齒,導(dǎo)致了文字會(huì)出現(xiàn)模糊。
參考文章:
【翻譯】:CSS 的 will-change 屬性詳解 - 前端 - 掘金
CSS3動(dòng)畫那么強(qiáng),requestAnimationFrame還有毛線用? ? 張鑫旭-鑫空間-鑫生活
你所不知道的setInterval | 晚晴幽草軒
深入理解requestAnimationFrame - 最騷的就是你 - 博客園