开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
今天学习如何给自己的音乐播放器配上同步的歌词字幕,并且过长的字幕还能够自动移动到视野中。
1、获取歌词和时长
从网易云app上获取歌词和字幕时长,并记录为json文件
json格式:
// lyrics文件
export default [
{ t: '2', w: 'There once was a ship that put to sea', c: '曾经有一艘船出海远航' },
{ t: '5', w: "And the name of that ship was the Billy o' Tea", c: '那艘船的名字是“比利·茶”号' },
{ t: '7', w: "The winds blew hard, her bow dipped down", c: '风刮得很大,船头向下倾' },
...
];
t表示播放开始时间单位秒,w是英文字幕,c是中文字幕,如果要更准确的设定字幕播放时长,还可以定义一个播放结束时间。
2、文字颜色移动
通过设置background-position属性来移动渐变颜色
...
background-image: linear-gradient(to right, #df5000 0%, #df5000 50%, #999 50%, #999 100%);
background-size: 200%;
-webkit-background-clip: text;
color: transparent;
background-position: 100% center;
animation: textColorMove 1s linear;
animation-fill-mode: forwards;
...
@keyframes textColorMove {
to {
background-position: 0% center;
}
}
3、歌词时间判断
首先要获取当前时间下的歌词:
<audio
...
onTimeUpdate={() => {
if (audioRef.current) {
...
getCurWord();
...
}
}}
...
监听Audio的onTimeUpdate事件,getCurWord函数获取当前时间下的歌词:
const getCurWord = () => {
if (!audioRef.current) return;
const curTime = audioRef.current.currentTime;
const { lyrics } = curSong;
const index = lyrics.findIndex((item: any, i: number) => {
return (
+item.t <= curTime &&
(i == lyrics.length - 1 || curTime < +lyrics[i + 1].t)
);
});
if (index != curWord) {
setCurWord(index);
onceRef.current = false;
once2Ref.current = false;
}
};
判断歌词json中 lyrics[i].t < audio.currentTime并且lyrics[i + 1].t > audio.currentTime。
歌词元素展示:
{curWord != -1 && curSong.lyrics[curWord] && (
<div
className={styles.word}
key={curWord + 'ol'}
style={{ animationDuration: offsetTime + 's' }}
ref={(dom) => setDomAnimation(dom, 0)}
>
{curWord != -1 && curSong.lyrics[curWord].w}
</div>
)}
key是必要的,当curWord切换时,因为key不同,元素div会重新渲染并且重置属性animation。setDomAnimation函数用来修改动画相关css变量。
wrapper是固定外壳宽度,width是文字长度。如果文字长度大于wrapper,视野中左移长度为width - wrapper,offsetTime为上下两个歌词的时间差。t1和t2两个时间段和等于offsetTime,offsetTime / width表示px/s,t1 = 左移长度 * px/s,t2表示停留时长。如果文字长度小于wrapper,t1等于0。
const setDomAnimation = (dom: HTMLDivElement | null, type: 0 | 1,) => {
const curref = type == 0 ? onceRef : once2Ref;
if (dom && !curref.current) {
curref.current = true;
const width = dom.clientWidth;
const lyricsDom = document.getElementById('lyrics');
const wrapper = lyricsDom?.clientWidth ?? 0;
if (width > wrapper) {
const t1 = Number(
(((width - wrapper) * offsetTime) / width).toFixed(1),
);
let time = offsetTime - t1;
time = time - 1 < 0 ? 0 : time - 1;
dom.style.setProperty('--offset', wrapper - width + 'px');
dom.style.setProperty('--time0', offsetTime + 's');
dom.style.setProperty('--time', t1 + 's');
dom.style.setProperty('--delay', time + 's');
setTimeout(() => {
if (!dom.className.includes(styles.animate)) {
dom.className = dom.className + ' ' + styles.animate;
}
}, 10);
}
}
}
.animate {
animation-name: textColorMove, textMove;
animation-duration: var(--time0), var(--time) !important;
animation-timing-function: linear;
animation-delay: 0s, var(--delay);
animation-fill-mode: forwards;
}
@keyframes textMove {
to {
left: var(--offset);
}
}
textMove动画中--offset定义文字过长时向左移动的距离。
Ending...