前言
这段时间写文章的频率有些低,因为一直在忙着构思并开发一个帮助竹笛爱好者(我自己)自学笛子的网站。又因为一些视频、音频资源的获取、存放以及省流的问题,也尝试了不少骚操作,虽然最终方案还是不太完美,但总的来说,基本功能算是都实现了。
这其中就有我期望值最大的两个功能:一个是在线节拍器,另一个是有机结合了伴奏和节拍器的曲谱页。
1. 展示效果
以下是独立的在线节拍器,为了能方便地在移动端操作,也可以全屏显示。
下面是集成到曲谱页面之后的效果,这里提供了更多维度的控制,使其可以与音乐和曲谱更加自然的融合。
整体效果还算可以吧?当然,声音肯定是听不到的了,有兴趣的可以点击 在线节拍器 亲自体验一下,如果能给出一些意见自然是最好的了。
曲谱中的节拍器由于涉及到自动跟音乐的节奏对齐,实现起来稍微麻烦一点,所有我今天主要想分享一下纯 JS + CSS
手搓独立的在线节拍器的一些思路,希望对大家开发类似的功能有些启发。
2. 功能要点
一个节拍器主要有以下几个功能要点:
- 可以通过滑块上下滑动来调节每分钟的节拍数(
BPM
),向上是减小(减慢),向下是增加(加快); - 将
BPM
换算成毫秒数,让摆针定时周期性左右摆动; - 摆针每次摆到正中间时,发出”哒哒“的节拍声。
虽然功能并不复杂,但具体实现起来,却还是有一些可以拿出来说一说的。
3. 实现思路
首先,很容易想到通过定位将节拍器的三个核心部件(主体、摆针、滑块)组合在一起,让它们不能随着屏幕的缩放而变形、错位。这很简单,唯一需要注意的是,由于滑块要在摆针上滑动,并且要和摆针一起摆动,因此,必须是摆针相对主体定位,而滑块又相对摆针定位。其HTML
结构与CSS
样式的核心代码大致如下:
<div class="metronome-box">
<div class="pendulum">
<div class="bpm-slider"></div>
</div>
</div>
<style>
.metronome-box {
position: relative;
background: url("../images/metronome.png") no-repeat ...;
// ...其他样式
}
.pendulum {
position: absolute;
background: url("../images/metronome.png") no-repeat ...;
// ...其他样式
}
.bpm-slider {
position: absolute;
background: url("../images/metronome.png") no-repeat ...;
// ...其他样式
}
</style>
这里主要用到了相对定位和绝对定位,以及精灵图来设置背景,这样主体结构就完成了。
3.1 滑块滑动
接下来要解决的核心问题,就是滑块了。有人可能会说,这还不简单吗,通过mousedown
、mousemove
和mouseup
事件,根据鼠标在屏幕上的移动动态设置滑块的top
值不就可以了吗?
确实是这样的,这也是我最开始想到的方法,并且已经落地实现了,只是比想象更复杂的是,滑块并不是从顶部划到底部,而是只在一个区间里滑动,并且要将滑动的区间按百分比换算成40
至208
之间(节拍器的有效数值范围)的整数,这里面涉及到大量坐标和高度的计算,一不小心就产生bug了。
废了九牛二虎之力,终于实现了,但测试的时候才意识到移动端根本就不识别mousedown
、mousemove
和mouseup
事件,需要实现对应的touchstart
、touchmove
和touchend
三个事件,虽然实现思路差不多,但其中获取屏幕高度和各种坐标的方式又不太一样,而且如果样式用到了缩放,鼠标定位还和实际显示效果不一致。到此我就觉得有点复杂了,可以就此打住了,虽然也能强行实现,但由此可能引入多少bug完全不可预料。
后面又尝试并排除出了三个很多人可能也会想到的备选方案:
- 引入第三方的拖拽
JS
库,这样可以保证最大的兼容性,最少的代码和最少的bug,但一个完整的第三方库放在这里给人一种高射炮打蚊子的感觉,不到迫不得已,我并不想引入。 - 摆针上的滑块不让滑了,在另外一个地方通过
range
类型的input
标签来实现滑动设值,就和曲谱页一样,只是这样体验就差了一些,但从功能上,它能完美满足我的需求,只需要实现input
事件即可,可以极大的减少复杂性。 - 依然想办法使用
range
标签,通过appearance
和其它样式将其改为纵向。但尝试之后发现,改为纵向后,有些横向生效的样式又不生效了。
不过,虽然以上三种方案都有问题,但却给了我灵感,就是通过旋转横向的range
标签实现纵向的效果。一试之下,确实可行,唯一的毛病就是视觉上的上下、宽高跟实际代码中写的是颠倒的了。
下面是最终方案的部分核心代码:
<div class="metronome-box">
<div class="pendulum">
<input type="range" min="40" max="208" step="1" class="bpm-slider">
</div>
</div>
<style>
.metronome-box {
position: relative;
background: url("../images/metronome.png") no-repeat ...;
// ...其他样式
}
.pendulum {
position: absolute;
background: url("../images/metronome.png") no-repeat ...;
// ...其他样式
}
.bpm-slider {
position: absolute;
background: url("../images/metronome.png") no-repeat ...;
-webkit-appearance: none;
appearance: none;
height: 54px;
width: 280px;
transform: rotate(90deg);
transform-origin: right bottom;
background: transparent;
// ...其他样式
}
.bpm-slider::-webkit-slider-thumb {
-webkit-appearance: none;
height: 54px;
width: 54px;
transform: rotate(-90deg);
background: url("../images/metronome.png") no-repeat ...;
// ...其他样式
}
</style>
我们这里用到的就是range
标签,核心思路就是把range
原本的样式去掉并且设置为透明背景,然后以右下角为圆心正向旋转90
度,最后再通过定位,将其移动到合适的位置。
当然,由于range
标签的轨道正向旋转了90
度,所以其滑块也跟着正向旋转了90
度,如果滑块的背景图不规则,或者有阴影效果,则会发现其形状不对,因此还需要逆向90
度旋转回去。
由此,一个兼容性好、实现简单又不容易出错的滑块就实现好了。
3.2 摆针摆动
这个比较简单,通过一个CSS
动画就可以实现,核心代码如下:
.pendulum {
animation: pendulum 1s infinite ease-in-out alternate;
animation-delay: -500ms;
transform-origin: 22px 392px;
}
@keyframes pendulum {
0% {
transform: rotate(30deg);
}
100% {
transform: rotate(-30deg);
}
}
我们可以通过transform-origin
设置摆动的圆心,这个不难理解,但需要注意的是,这个摆针的起始位置是右边30
度,而结束位置是左边30
度,但实际情况,摆针的开始和结束位置都应该在正中间,因此,我们还需要将animation-delay
设置成动画周期一半的负数值,以达到这个效果。上面示例中的-500ms
就表示立即从500ms
处开始执行,也就是一开始摆针就提前摆了一半。当然,每次根据滑块的值设置动画周期的时候,也需要相应的调整animation-delay
的值。
最后还有一点需要注意,CSS
动画只有暂停没有停止,但暂停是不对的,真实的节拍器也没有暂停效果,因此,启动和停止可以通过JS
修改animation-name
实现,如下面代码所示:
if (开始的条件) {
pendulum.style.animationName = "pendulum";
} else {
pendulum.style.animationName = "none";
}
3.3 声音同步
由于独立的节拍器不涉及与音乐的节奏同步问题,因此实现思路比较简单,只需要将滑块设置的BPM
值换算成毫秒数,然后通过setInterval()
函数定时发出声响即可。只是为了发出“哒哒”声,这里需要用到AudioContext
这个HTML5
的多媒体API
,AudioContext
使用起来并不复杂,但其本身也涉及到了较多的概念和API
,所以就不在这里继续展开了,有兴趣的可以通过MDN
搜索AudioContext
进一步了解。
结语
好了,这次分享就到这里,虽然功能单个分开都不复杂,只涉及到了CSS
定位、旋转、动画以及多媒体API
,但组合起来或许能给你带来一些参考。同时,实现的过程和思路也或许能给你带来一些启示,很多时候开发过程中遇到了技术难题,不一定要立马去想攻克的办法,换个思路,或许就能找到更简单也更有效的解决办法。
评论0
暂时没有评论