马黑黑 发表于 2022-9-25 17:00

用元素覆盖方式模拟歌词逐字同步效果(演示)

<style>
        #papa { left: -214px; width: 1024px; height: 640px; background: gray url('/data/attachment/forum/202209/25/072505jvpqdny33qoxvhoo.jpg') no-repeat center/cover; box-shadow: 3px 3px 20px #000; box-shadow: 3px 3px 20px #000; display: grid; place-items: center; overflow: hidden; position: relative; z-index: 1; }
        #mplayer { position: absolute; right: 40px; bottom: 0; width: 300px; height: 80px; user-select: none; display: grid; place-items: center; cursor: pointer; }
        #mplayer:hover #btnwrap, #mplayer:hover #prog { transform: translateY(var(--yy)); }
        #mplayer:hover #btnwrap { background: linear-gradient(to top right, purple, orange); border-radius: 50%; opacity: .55; }
        #btnwrap, #prog { position: absolute; display: grid; place-items: center; transition: .5s; }
        #btnwrap { --yy: -15px; width: 40px; height: 40px; transform: rotate(45deg); border: 1px solid tan; border-radius: 6px; opacity: 0; }
        #btnplay { width: 20px; height: 20px; transform: translateX(3px); background: #eee; clip-path: polygon(0 0, 0% 100%, 100% 50%); }
        #btnpause { width: 2px; height: 20px; border-style: solid; border-width: 0px 4px; border-color: transparent #eee; display: none; }
        #prog { --yy: 20px; width: 300px; height: 16px; border-radius: 10px; background: linear-gradient(90deg, orange, purple 100%, transparent 0); border: 1px solid teal; font: normal 14px / 16px sans-serif; color: #ccc; text-shadow: 1px 1px 1px #000; opacity: .65; }
        #lrcwrap { position: absolute; top: 20px; font: bold 2.4em 'KaiTi',sans-serif; }
        #lrc1, #lrc2 { width: 100%; height: 100%; color: snow; white-space: nowrap; }
        #lrc2 { position: absolute; top: 0; left: 0; color: purple; overflow: hidden; }
        #mpic {position: absolute; transform: rotateY(180deg); left: 0; top: 0; width: 100px; offset-distance: 0; offset-path: path("M 0 0 Q 300 90, 512 100 T 1024 30"); animation: fly 18s linear infinite;}
        @keyframes fly { to { offset-distance: 100%; } }
        @keyframes mov1 { from { width: 0%;} to { width: 100%; } }
        @keyframes mov2 { from { width: 0%;} to { width: 100%; } }

</style>

<div id="papa" data-lr="no">
        <img id="mpic" alt="" src="/data/attachment/forum/202209/25/072652ckuqwlat7wqk6o6z.gif" />
        <div id="lrcwrap">
                <span id="lrc1">花潮lrc在线</span>
                <span id="lrc2">花潮lrc在线</span>
        </div>
        <div id="mplayer">
                <div id="btnwrap"><span id="btnplay"></span><span id="btnpause"></span></div>
                <div id="prog">00:00 | 00:00</div>
        </div>
</div>

<script>
let mKey = 0, mFlag = true, aud = new Audio();
let lrcAr = [,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,];
aud.src = 'https://music.163.com/song/media/outer/url?id=1978686514.mp3';
aud.autoplay = true;
aud.loop = true;
btnwrap.onclick = () => aud.paused ? aud.play() : aud.pause();
prog.onclick = (e) => aud.currentTime = aud.duration * e.offsetX / prog.offsetWidth;
aud.addEventListener('pause', () => mState());
aud.addEventListener('play', () => mState());
aud.addEventListener('seeked', () => calcKey());
aud.addEventListener('timeupdate', () => {
        prog.style.background = 'linear-gradient(90deg, orange, purple ' + aud.currentTime / aud.duration * 100 + '%, snow 0)';
        prog.innerText = toMin(aud.currentTime) + ' | ' + toMin(aud.duration);
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime >= lrcAr) {
                        if (mKey === j) showLrc(lrcAr);
                        else continue;
                }
        }
});
let mState = () => aud.paused ? (btnplay.style.display = 'block', btnpause.style.display = 'none', lrc2.style.animationPlayState = 'paused') : (btnplay.style.display = 'none', btnpause.style.display = 'block', lrc2.style.animationPlayState = 'running');
let showLrc = (time) => {
        lrc2.style.animation = (mFlag ? 'mov1 ' : 'mov2 ') + time + 's linear forwards';
        lrc1.innerHTML = lrc2.innerHTML = lrcAr;
        mKey += 1;
        mFlag = !mFlag;
}
let calcKey = () => {
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime <= lrcAr) {
                        mKey = j - 1;
                        break;
                }
        }
        if (mKey < 0) mKey = 0;
        if (mKey > lrcAr.length - 1) mKey = lrcAr.length - 1;
        let mtime = lrcAr - (aud.currentTime - lrcAr);
        showLrc(mtime);
};
let toMin = (val) => {
        if (!val) return '00:00';
        val = Math.floor(val);
        let min = parseInt(val / 60),
                sec = parseFloat(val % 60);
        if (min < 10) min = '0' + min;
        if (sec < 10) sec = '0' + sec;
        return min + ':' + sec;
};

</script>

马黑黑 发表于 2022-9-25 17:00

本帖最后由 马黑黑 于 2022-9-25 17:01 编辑

全帖代码<style>
        #papa { left: -214px; width: 1024px; height: 640px; background: gray url('/data/attachment/forum/202209/25/072505jvpqdny33qoxvhoo.jpg') no-repeat center/cover; box-shadow: 3px 3px 20px #000; box-shadow: 3px 3px 20px #000; display: grid; place-items: center; overflow: hidden; position: relative; z-index: 1; }
        #mplayer { position: absolute; right: 40px; bottom: 0; width: 300px; height: 80px; user-select: none; display: grid; place-items: center; cursor: pointer; }
        #mplayer:hover #btnwrap, #mplayer:hover #prog { transform: translateY(var(--yy)); }
        #mplayer:hover #btnwrap { background: linear-gradient(to top right, purple, orange); border-radius: 50%; opacity: .55; }
        #btnwrap, #prog { position: absolute; display: grid; place-items: center; transition: .5s; }
        #btnwrap { --yy: -15px; width: 40px; height: 40px; transform: rotate(45deg); border: 1px solid tan; border-radius: 6px; opacity: 0; }
        #btnplay { width: 20px; height: 20px; transform: translateX(3px); background: #eee; clip-path: polygon(0 0, 0% 100%, 100% 50%); }
        #btnpause { width: 2px; height: 20px; border-style: solid; border-width: 0px 4px; border-color: transparent #eee; display: none; }
        #prog { --yy: 20px; width: 300px; height: 16px; border-radius: 10px; background: linear-gradient(90deg, orange, purple 100%, transparent 0); border: 1px solid teal; font: normal 14px / 16px sans-serif; color: #ccc; text-shadow: 1px 1px 1px #000; opacity: .65; }
        #lrcwrap { position: absolute; top: 20px; font: bold 2.4em 'KaiTi',sans-serif; }
        #lrc1, #lrc2 { width: 100%; height: 100%; color: snow; white-space: nowrap; }
        #lrc2 { position: absolute; top: 0; left: 0; color: purple; overflow: hidden; }
        #mpic {position: absolute; transform: rotateY(180deg); left: 0; top: 0; width: 100px; offset-distance: 0; offset-path: path("M 0 0 Q 300 90, 512 100 T 1024 30"); animation: fly 18s linear infinite;}
        @keyframes fly { to { offset-distance: 100%; } }
        @keyframes mov1 { from { width: 0%;} to { width: 100%; } }
        @keyframes mov2 { from { width: 0%;} to { width: 100%; } }

</style>

<div id="papa" data-lr="no">
        <img id="mpic" alt="" src="/data/attachment/forum/202209/25/072652ckuqwlat7wqk6o6z.gif" />
        <div id="lrcwrap">
                <span id="lrc1">花潮lrc在线</span>
                <span id="lrc2">花潮lrc在线</span>
        </div>
        <div id="mplayer">
                <div id="btnwrap"><span id="btnplay"></span><span id="btnpause"></span></div>
                <div id="prog">00:00 | 00:00</div>
        </div>
</div>

<script>
let mKey = 0, mFlag = true, aud = new Audio();
let lrcAr = [,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,];
aud.src = 'https://music.163.com/song/media/outer/url?id=1978686514.mp3';
aud.autoplay = true;
aud.loop = true;
btnwrap.onclick = () => aud.paused ? aud.play() : aud.pause();
prog.onclick = (e) => aud.currentTime = aud.duration * e.offsetX / prog.offsetWidth;
aud.addEventListener('pause', () => mState());
aud.addEventListener('play', () => mState());
aud.addEventListener('seeked', () => calcKey());
aud.addEventListener('timeupdate', () => {
        prog.style.background = 'linear-gradient(90deg, orange, purple ' + aud.currentTime / aud.duration * 100 + '%, snow 0)';
        prog.innerText = toMin(aud.currentTime) + ' | ' + toMin(aud.duration);
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime >= lrcAr) {
                        if (mKey === j) showLrc(lrcAr);
                        else continue;
                }
        }
});
let mState = () => aud.paused ? (btnplay.style.display = 'block', btnpause.style.display = 'none', lrc2.style.animationPlayState = 'paused') : (btnplay.style.display = 'none', btnpause.style.display = 'block', lrc2.style.animationPlayState = 'running');
let showLrc = (time) => {
        lrc2.style.animation = (mFlag ? 'mov1 ' : 'mov2 ') + time + 's linear forwards';
        lrc1.innerHTML = lrc2.innerHTML = lrcAr;
        mKey += 1;
        mFlag = !mFlag;
}
let calcKey = () => {
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime <= lrcAr) {
                        mKey = j - 1;
                        break;
                }
        }
        if (mKey < 0) mKey = 0;
        if (mKey > lrcAr.length - 1) mKey = lrcAr.length - 1;
        let mtime = lrcAr - (aud.currentTime - lrcAr);
        showLrc(mtime);
};
let toMin = (val) => {
        if (!val) return '00:00';
        val = Math.floor(val);
        let min = parseInt(val / 60),
                sec = parseFloat(val % 60);
        if (min < 10) min = '0' + min;
        if (sec < 10) sec = '0' + sec;
        return min + ':' + sec;
};
</script>


马黑黑 发表于 2022-9-25 17:00

本帖最后由 马黑黑 于 2022-9-25 17:40 编辑

实现细节分析

给lrc的dom节点做了一些改造,显示歌词的HTML代码如下:

      <div id="lrcwrap">
                <span id="lrc1">花潮lrc在线</span>
                <span id="lrc2">花潮lrc在线</span>
      </div>


其中,id="lrcwrap" 的 div 是包裹歌词的父元素,它的 CSS 设定非常简单——

#lrcwrap {
      position: absolute;
      top: 20px;
      font: bold 2.4em 'KaiTi',sans-serif;
}


就是绝对定位、物理定位(left、top等值设定),以及字体。字体的设定将被其内的子元素所继承,所以后面的子元素不再设置字体。其下子元素都是 span 元素,共两个,一个 id 为 lrc1,另一个 id 是 lrc2,我们就讲将它们:

lrc1 和 lrc2 有共同的 CSS 样式,也不复杂——

#lrc1, #lrc2 {
      width: 100%;
      height: 100%;
      color: snow;
      white-space: nowrap;

}

先是高宽设置,都用 100% 来表示,接着是前景色和不折行设置。lrc1不设置定位,默认就是 relative,由它来撑开父元素 lrcwrap 的宽度。当父元素绝对定位、不设置宽度时,其实际宽度由其内某个最大尺寸的元素决定。这会很绕,但这正是我们利用的原理所在:子元素100%的尺寸,初始时其实是0(受制于父元素的影响,父元素此时没有尺寸),然后当 lrc1 有了文本,由于 lrc1 不是绝对定位,有了内容就有尺寸,就撑开了父元素。这就等于:子元素给了父元素尺寸,父元素又规定了子元素只能是这个尺寸(100%)。

而 lrc2,它还要额外设定一下 CSS 样式——

#lrc2 {
      position: absolute;
      top: 0;
      left: 0;
      color: purple;
      overflow: hidden;
}


给它一个绝对定位,且固定到父元素的 {0,0} 坐标处,这样,它就能覆盖按文本流自然状态摆放的哥哥 lrc1 的位置,接着设置不同于兄长元素的前景色(color: purple),再加入防溢出属性设定(overflow),然后我们就可以通过控制它的宽度,从而达到逐渐覆盖兄长元素的目的。

相应的JS当然也要做一些改动,主要是 lrcShow(time) 的封装,颜色代码部分是修改的:

let showLrc = (time) => {
      lrc2.style.animation = (mFlag ? 'mov1 ' : 'mov2 ') + time + 's linear forwards';
      lrc1.innerHTML = lrc2.innerHTML = lrcAr;
      mKey += 1;
      mFlag = !mFlag;
}


严格来讲,仅修改了红色那一句,暗红色的那句只是因为调用动画的元素 和 @keyframes 动画名称都发生了变化而跟着改变而已。红色那句,原来是酱紫:

      lrc.innerHTML = lrcAr;

可以看出来吧?赋值给一个元素的文本,修改后变成赋值给两个元素而已。

马黑黑 发表于 2022-9-25 17:01

本帖最后由 马黑黑 于 2022-9-25 19:24 编辑

关于 lrc2 的物理定位:

有一个小技巧,就是,top: -1px,left: -1px,或用正1,或正1负1配合,加之两个盒子的文本颜色设置得当,两个元素轻微的偏移,可以取得意想不到的效果。可以一试。

红影 发表于 2022-9-25 18:31

黑黑找到了第三种另歌词逐字同步的方法,厉害{:4_199:}

马黑黑 发表于 2022-9-25 19:20

红影 发表于 2022-9-25 18:31
黑黑找到了第三种另歌词逐字同步的方法,厉害

这个法子也非常不错,兼容性可能是最好的

红影 发表于 2022-9-25 22:44

马黑黑 发表于 2022-9-25 19:20
这个法子也非常不错,兼容性可能是最好的

太好了,黑黑的歌词同步越做越完美了{:4_199:}

马黑黑 发表于 2022-9-25 23:18

红影 发表于 2022-9-25 22:44
太好了,黑黑的歌词同步越做越完美了

也不叫完美。核心技术是真正的逐字同步,我们这个只是模拟

红影 发表于 2022-9-26 10:33

马黑黑 发表于 2022-9-25 23:18
也不叫完美。核心技术是真正的逐字同步,我们这个只是模拟

用简洁的代码,就能模拟得这么好,可以堪称完美了{:4_199:}

马黑黑 发表于 2022-9-26 12:10

红影 发表于 2022-9-26 10:33
用简洁的代码,就能模拟得这么好,可以堪称完美了

自我满足{:4_170:}

红影 发表于 2022-9-26 16:09

马黑黑 发表于 2022-9-26 12:10
自我满足

真的很好啊,黑黑对自己要求太高了{:4_173:}

马黑黑 发表于 2022-9-26 18:17

红影 发表于 2022-9-26 16:09
真的很好啊,黑黑对自己要求太高了

我对自己其实很随意的

红影 发表于 2022-9-26 19:25

马黑黑 发表于 2022-9-26 18:17
我对自己其实很随意的

今天去做歌词,看到黑黑最后那个很完美啊,做完连加餐都一起完成了呢{:4_187:}

马黑黑 发表于 2022-9-26 19:31

红影 发表于 2022-9-26 19:25
今天去做歌词,看到黑黑最后那个很完美啊,做完连加餐都一起完成了呢

但别忘了要做微调

红影 发表于 2022-9-26 19:39

马黑黑 发表于 2022-9-26 19:31
但别忘了要做微调

做微调也很方便,直接点过去,做好的就都带过去了,太棒了{:4_199:}

马黑黑 发表于 2022-9-26 19:40

红影 发表于 2022-9-26 19:39
做微调也很方便,直接点过去,做好的就都带过去了,太棒了

设计的有点巧妙呢,我自己都佩服我自己{:4_173:}

红影 发表于 2022-9-26 20:01

马黑黑 发表于 2022-9-26 19:40
设计的有点巧妙呢,我自己都佩服我自己

黑黑的巧妙设计,使用起来特别方便呢{:4_199:}

马黑黑 发表于 2022-9-26 20:23

红影 发表于 2022-9-26 20:01
黑黑的巧妙设计,使用起来特别方便呢

界面的设计也重来了,不那么凌乱

红影 发表于 2022-9-27 20:44

马黑黑 发表于 2022-9-26 20:23
界面的设计也重来了,不那么凌乱

现在有三种歌词逐字同步的代码了,大家可以按自己喜欢的做帖了{:4_173:}

马黑黑 发表于 2022-9-27 21:14

红影 发表于 2022-9-27 20:44
现在有三种歌词逐字同步的代码了,大家可以按自己喜欢的做帖了

这三种,后两种实际上是同性质的
页: [1] 2
查看完整版本: 用元素覆盖方式模拟歌词逐字同步效果(演示)