马黑黑 发表于 2024-2-5 08:42

range进度条播放器+LRC歌词同步教程(五)

<style>
.mum { font-size: 18px; }
.mum pre { padding: 12px; background: #eee; color: navy; font: normal 16px/20px Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; tab-size: 4; white-space: pre-wrap; word-wrap: break-word; }
.mum a { color: red; }
.mum a:hover { color: darkred; }
.tGreen { color: green; }
.tRed { color: red; }
</style>

<div class="mum">

<p>本讲将是本系列教程的完结篇。事实上,在上一讲,<a href="http://mhh.52qingyin.cn/art/show.php?st=1&sd=1&art=mahei_1707008224" target="_blank">《range进度条播放器+LRC歌词同步教程(四)》</a>,播放器的核心功能都已实现,可以作为终结篇看待,不过我们还想略作扩展,主要是两方面的内容:一是响应键盘操作,二是一些细节优化、完善与扩充。</p>
<p>input 组件的 type="range" 滑杆元素具备获得焦点功能,当它获得焦点,键盘的方向键可以调节range滑杆进度,对播放器而言,按任意方向键都会使得音频或视频快进和快退,如果不为之做编程处理也无伤大雅,就是体验上有所欠缺,例如连续快进、快退时出现的倒带破音,故此还是处理一下为好。JS针对键盘键位编程提供三个事件:onkeydown(键位按下)、keyup(键位弹起)和 keypress(有输入值的键位正常按下松开),我们只需使用前两个就好。按下弹起的是哪一个键,JS能做出判断,看例子和解释就能明白:</p>
<pre>
&lt;p id="msg"&gt;请输入:&lt;/p&gt;
&lt;p&gt;&lt;input id="mybox" type="text" value=""/&gt;&lt;/p&gt;

&lt;script&gt;
mybox.onkeydown = (e) =&gt; msg.innerText = e.code;

<span class="tGreen">/* [说明] e.code 获得键位码的明文值,e.key 获得另外一种键位码的明文值;e.repeat 还能检测是否长按某一个键。 */</span>
&lt;/script&gt;
</pre>
<p>运行以上代码,令文本框获得焦点后,敲击键盘上任意一个键,消息框都会显示该键位的键位码。比如敲个空格,显示 Space,左方向键,显示 ArrowLeft,右方向键,显示 ArrowRight,它甚至能区分左右上档键、主键盘数字和小键盘数字。除了用 e.code 这样的关键字获取键位码,还可以用 e.key 和 e.keyCode 获得数字类型的键位码(keyCode将被废弃),这里我们决定使用 e.code。</p>
<p>由于键位按下允许长按,按下不放的过程会反复触发 range 进度条的 onchange 事件,而本讲以前的代码我们用 onchange 事件来实现进度条手动变更后的播放出播放进度调节,这样,方向键被长按时播放器就会出现快进快退的破音,所以 onchange 事件的代码交给一个函数:</p>
<pre>
<span class="tGreen">/* 原来的 mprog onchange 事件 */</span>
<span style="text-decoration: line-through;text-decoration-color: red;">mprog.onchange = () => aud.currentTime = aud.currentTime = mprog.value / mprog.max * aud.duration;</span>

<span class="tGreen">/* 改为函数 setChange*/</span>
var setChange = () => {
        aud.currentTime = mprog.value / mprog.max * aud.duration;
        mseek = false; <span class="tGreen">/* 进度条控制权交还audio控件 */</span>
}

<span class="tGreen">/* 然后鼠标、键盘松开事件分别调用上述函数 */</span>
mprog.onmouseup = () => setChange();
mprog.onkeyup = (e) => { if(e.code.toLowerCase().includes('arrow')) setChange(); };

<span class="tGreen">/* 键位按下事件 :令 mseek 为真(和鼠标按下一样道理) */</span>
mprog.onkeydown = (e) => { if(e.code.toLowerCase().includes('arrow')) mseek = true; }

<span class="tGreen">/*[<span class="tRed">注意</span>] 键位按下、弹起事件只针对方向键,所以有一个if语句 */</span>

<span class="tGreen">/* 此外,使用 oninput 事件替代 onchange 用于实时显示手动改变进度条的数据,前者在此兼容鼠标键盘的性能更好*/</span>
mprog.oninput = () => mplayer.dataset.tt = toMin(mprog.value / mprog.max * aud.duration) + ' ' + toMin(aud.duration);
</pre>
<p>这样处理以后,短按、长按方向键的操作效果,和用鼠标拖曳滑杆滑块的操作效果一致。按键相应功能至此解决,但为了提供更为友好的操作体验,我们还应提供进度条获得焦点的样式,这在 CSS 中处理,加一个伪类 :focus 即可:</p>
<pre>
#mprog:focus { accent-color: gold; } <span class="tGreen">/* 进度条获得焦点强调色 */</span>
</pre>
<p>那如何让进度条获得焦点?鼠标点击,以及,页面处于活动中时按键盘上的 Tab 键,都能让拥有焦点能力的元素获得焦点,鼠标点击马上获得,按 Tab键 则根据元素的 tabindex 属性值依次获得。获得焦点后,元素将按 :focus 伪类渲染。</p>
<p>[<span class="tRed">提示</span>]如果不想让进度条获得焦点,简单的做法是在HTML代码中的对应元素加入一个JS语句,<mark>onfocus="blur()"</mark>,意思是一旦获得焦点就立马失去焦点,这样的话,上面基于键位事件的编程就没有必要了。</p>
<p>下来谈谈细节完善与扩展问题。</p>
<p>教程此前的代码,均设定父元素为 id="papa",很多时候我们可能想使用别的,甚至可能不想在帖子元素上使用id,那我们需要处理一下传递CSS变量 --state 由谁来承载和传递的问题。CSS变量具备继承性,只有处理得当,继承性还可以渗透到子孙元素的伪元素,所以,如果没有了 papa,我们可以找个更大的靠山,body,页面舞台的顶级父元素(但不是HTML顶级元素,就是说比body大的还有,只是它们不是能呈现样貌的元素)。看代码:</p>
<pre>
var pa = document.querySelector('body'); <span class="tGreen">/* 声明pa变量 → body */</span>

<span class="tGreen">/* mState 函数修改为 */</span>
var mState = () => aud.paused ?
        (<span class="tRed">pa</span>.style.setProperty('--state', 'paused'), btnplay.title = '点击播放') :
        (<span class="tRed">pa</span>.style.setProperty('--state', 'running'), btnplay.title = '点击暂停');
       
<span class="tGreen">/* 这样,凡依赖于 --state 变量运行关键帧动画的元素,均被管到(除非在不合适的地方额外设置了 --state 值) */</span>
</pre>
<p>此外,我们可能希望修饰帖子的视频也能随音频而播放或暂停,若此,我们可以设计一个函数:</p>
<pre>
<span class="tGreen">/* 控制视频函数 */</span>
var ctrVids = (stop) => {
        let vids = document.querySelectorAll('video'); <span class="tGreen">/* 获得所有视频操作句柄 */</span>
        vids.forEach(vid => stop ? vid.pause() : vid.play()); <span class="tGreen">/* 根据stop参数(布尔)控制视频 */</span>
};

<span class="tGreen">/* 完善 mState 函数 : 调用上述函数 */</span>
var mState = () => aud.paused ?
        (<span class="tRed">pa</span>.style.setProperty('--state', 'paused'), btnplay.title = '点击播放', ctrVids(true)) :
        (<span class="tRed">pa</span>.style.setProperty('--state', 'running'), btnplay.title = '点击暂停', ctrVids(false));
</pre>
<p>整理全部代码如下:</p>
<pre>
<span class="tRed">&lt;!-- CSS代码 --&gt;</span>
&lt;style&gt;
.papa {
        margin: auto;
        width: 800px;
        height: 360px;
        background: linear-gradient(tan,gray);
        box-shadow: 3px 3px 20px #000;
        position: relative;
        display: grid;
        place-items: center;
}
#lrc {
        position: absolute;
        top: 10px;
        font: bold 2.4em sans-serif;
        color: lightblue;
        text-shadow: 1px 1px 1px rgba(0,0,0,.45);
        --ani: lrcGo1;
        --duration: 1s;
}
#lrc::before {
        position: absolute;
        content: attr(data-lrc);
        width: 100%;
        height: 100%;
        color: transparent;
        background: linear-gradient(rgba(250,0,0,.7),rgba(0,0,180,.8));
        background-clip: text;
        -webkit-background-clip: text;
        clip-path: inset(0 100% 0 0);
        animation: var(--ani) var(--duration) linear forwards var(--state);
        border-bottom: 1px solid navy;
}
#mplayer {
        position: absolute;
        bottom: 10px;
        text-align: center;
}
#mplayer::before {
        position: absolute;
        content: attr(data-tt);
        left: 0;
        bottom: 25px;
        width: 100%;
        text-align-last: justify;
}
#mprog {
        width: 240px;
        accent-color: darkgreen;
        outline: none;
        cursor: pointer;
}
#mprog:focus {
        accent-color: gold;
}
#btnplay {
        width: 80px;
        height: 80px;
        cursor: pointer;
        animation: rotating 6s infinite linear var(--state);
}
@keyframes rotating { to { transform: rotate(360deg); } }
@keyframes lrcGo0 { to { clip-path: inset(0 0 0 0); } }
@keyframes lrcGo1 { to { clip-path: inset(0 0 0 0); } }
&lt;/style&gt;

<span class="tRed">&lt;!-- HTML代码 --&gt;</span>
&lt;div class="papa"&gt;
        &lt;audio id="aud" src="https://music.163.com/song/media/outer/url?id=212524" autoplay loop&gt;&lt;/audio&gt;
        &lt;div id="mplayer" data-tt="0:00 0:00"&gt;
                &lt;img id="btnplay" src="https://638183.freep.cn/638183/small/002_133507167677724892.png" title="播放/暂停" alt="" /&gt;&lt;br&gt;
                &lt;input id="mprog" type="range" min="0" max="100" step="any" value="0" title="调节进度" /&gt;
        &lt;/div&gt;
        &lt;div id="lrc" data-lrc="HuaChao LRC"&gt;HuaChao LRC&lt;/div&gt;
&lt;/div&gt;

<span class="tRed">&lt;!-- JS代码 --&gt;</span>
&lt;script&gt;
<span class="tGreen">/* mseek 手动调节 :初始为假;aniIdx 动画名索引;lrcKey 当前处理的歌词索引 */</span>
var mseek = false, aniIdx = 0, lrcKey = 0;
var pa = document.querySelector('body'); <span class="tGreen">/* --state变量交由 body 传递 */</span>

<span class="tGreen">/* 函数 - 格式化分 → 分:秒 */</span>
var toMin = (val) =&gt; {
        if(!val) return '0:00';
        var min = parseInt(val / 60), sec = Math.floor(val) % 60;
        if(sec &lt; 10) sec = '0' + sec;
        return min + ':' + sec;
};

<span class="tGreen">/* 函数 - 状态控制,含依赖 --state 的CSS动画、视频、播放按钮标题 */</span>
var mState = () =&gt; aud.paused ?
        (pa.style.setProperty('--state', 'paused'), btnplay.title = '点击播放', ctrVids(true)) :
        (pa.style.setProperty('--state', 'running'), btnplay.title = '点击暂停', ctrVids(false));

<span class="tGreen">/* 函数 - lrc歌词同步 */</span>
var showLrc = (time) =&gt; {
        lrc.textContent = lrc.dataset.lrc = geci.replace(/&lt;br&gt;/, '\n');
        lrc.style.setProperty('--ani', ['lrcGo0','lrcGo1']);
        lrc.style.setProperty('--duration', time + 's');
        pa.style.setProperty('--state', 'running');
        aniIdx = aniIdx === 0 ? 1 : 0;
        lrcKey ++;
};

<span class="tGreen">/* 函数 - 计算 lrcKey 值 */</span>
var calcKey = () =&gt; {
        for (let j = 0; j &lt; geci.length; j++) {
                if (aud.currentTime &lt;= geci) {
                        lrcKey = j - 1;
                        break;
                }
        }
        if (lrcKey &lt; 0) lrcKey = 0;
        if (lrcKey &gt; geci.length - 1) lrcKey = geci.length - 1;
        let time = geci - (aud.currentTime - geci);
        showLrc(time);
        pa.style.setProperty('--state', aud.paused ? 'paused' : 'running');
};

<span class="tGreen">/* 函数 - 手动改变进度 */</span>
var setChange = () =&gt; {
        aud.currentTime = mprog.value / mprog.max * aud.duration;
        mseek = false; <span class="tGreen">/* 进度条控制权交还audio控件 */</span>
}

<span class="tGreen">/* 函数 - 控制视频播放/暂停 */</span>
var ctrVids = (stop) =&gt; {
        let vids = document.querySelectorAll('video'); <span class="tGreen">/* 获得所有视频操作句柄 */</span>
        vids.forEach(vid =&gt; stop ? vid.pause() : vid.play()); <span class="tGreen">/* 根据stop参数(布尔)控制视频 */</span>
};

<span class="tGreen">/* audio 相关监听事件 */</span>
aud.addEventListener('pause', () =&gt; mState()); <span class="tGreen">/* onpause */</span>
aud.addEventListener('playing', () =&gt; mState()); <span class="tGreen">/* onplaying */</span>
aud.addEventListener('seeked', () =&gt; calcKey()); <span class="tGreen">/* onseeked */</span>
<span class="tGreen">/* ontimeupdate */</span>
aud.addEventListener('timeupdate', () =&gt; {
        <span class="tGreen">/* 非手动调节进度时驱动进度变化 */</span>
        if(!mseek) mprog.value = aud.currentTime / aud.duration * mprog.max;
        mplayer.dataset.tt = toMin(aud.currentTime) + ' ' + toMin(aud.duration);
        for(var j = 0; j &lt; geci.length; j ++) {
                if (aud.currentTime &gt;= geci) {
                        if (j === lrcKey ) showLrc(geci);
                }
        }
});

<span class="tGreen">/* 进度条上的鼠标与键位事件 */</span>
mprog.onmousedown = () =&gt; mseek = true;
mprog.onmouseup = () =&gt; setChange();
mprog.onkeydown = (e) =&gt; { if(e.code.toLowerCase().includes('arrow')) mseek = true; }
mprog.onkeyup = (e) =&gt; { if(e.code.toLowerCase().includes('arrow')) setChange(); };
/* 进度条输入事件 - 实时显示手动改变进度数据*/
mprog.oninput = () => mplayer.dataset.tt = toMin(mprog.value / mprog.max * aud.duration) + ' ' + toMin(aud.duration);

<span class="tGreen">/* 播放按钮点击事件 */</span>
btnplay.onclick = () =&gt; aud.paused ? aud.play() : aud.pause();

<span class="tGreen">/* 歌词数组 :注意,这里必须使用 geci 变量名 */</span>
var geci = [ , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ];

&lt;/script&gt;
</pre>

</div>

幸运草 发表于 2024-2-5 09:04

我系来点赞滴{:6_244:}

红影 发表于 2024-2-5 10:28

原来键盘上的是针对方向键的。{:4_204:}

红影 发表于 2024-2-5 10:30

这样一行行的代码诗句,把这个range进度条结合歌词同步的设计过程讲得如此透彻,太赞了{:4_199:}

红影 发表于 2024-2-5 10:33

(pa.style.setProperty(……之前不知道这个pa怎来的,原来它是被声明了的更大靠山body {:4_187:}

红影 发表于 2024-2-5 10:35

原本替换掉的内容还用了划改线,这样很便于比较呢。真好。
还有专门的控制视频的,控制鼠标和键盘的以及按钮的,如此豪华的阵容,保证了运行的完美{:4_199:}

红影 发表于 2024-2-5 10:35

黑黑辛苦了{:4_190:}{:4_184:}

马黑黑 发表于 2024-2-5 11:48

红影 发表于 2024-2-5 10:35
黑黑辛苦了

{:4_173:}

樵歌 发表于 2024-2-5 11:53

加完分分吃瓜皮去{:4_334:}

马黑黑 发表于 2024-2-5 12:16

樵歌 发表于 2024-2-5 11:53
加完分分吃瓜皮去

瓜皮降噪

红影 发表于 2024-2-5 15:54

马黑黑 发表于 2024-2-5 11:48


完整版,顶起来{:4_178:}

马黑黑 发表于 2024-2-5 18:26

红影 发表于 2024-2-5 15:54
完整版,顶起来

谢顶

红影 发表于 2024-2-5 21:12

马黑黑 发表于 2024-2-5 18:26
谢顶

没头发的意思么{:4_170:}
页: [1]
查看完整版本: range进度条播放器+LRC歌词同步教程(五)