让我们用 JavaScript 💻🤘 构建一把真正能用的吉他🎸
锻造乐器
将吉他弦夹紧到位
把放大器调大!
编辑:抓住机会!
总结
我们来做一把吉他吧!好吧,不是实体吉他,而是下一个最好的选择:电子吉他!心动了吗?好!就像一场精彩的摇滚演出,不妨一试!
锻造乐器
我先从一些样板开始:一个简单的 HTML 文件,内嵌一个 SVG 图像。之所以用内嵌,是因为之后我需要添加很多 JS 代码。我一直很喜欢Gibson Flying V的设计,所以我会从它的头部和颈部开始创作。我先从一些线性渐变和一个阴影滤镜开始:
<svg id="guitar" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2400 800" preserveAspectRatio="xMidYMid meet" width="2400" height="800">
<defs>
<linearGradient id="fretboard" x1="42%" y1="0%" x2="0%" y2="90%">
<stop offset="0%" style="stop-color: rgb(56, 53, 53);" />
<stop offset="100%" style="stop-color: rgb(56, 49, 43);" />
</linearGradient>
<linearGradient id="fredboardBorder" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: rgb(111, 111, 111);" />
<stop offset="53%" style="stop-color: rgb(255, 255, 255);" />
<stop offset="100%" style="stop-color: rgb(160, 160, 160);" />
</linearGradient>
<linearGradient id="fret" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color: rgb(122, 117, 113);" />
<stop offset="100%" style="stop-color: rgb(56, 49, 43);" />
</linearGradient>
<filter id="dropshadow" height="400%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="4" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="1.5"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- ... -->
</svg>
我使用多边形作为基本结构,使用矩形和多边形作为琴弦,使用路径作为音品:
<svg ...>
<!-- ... -->
<polygon
points="
-10,300 1860,300 1950,230 2380,400
1950,570 1860,500 -10,500
"
fill="url(#fretboard)"
stroke-width="10"
stroke="url(#fredboardBorder)"
style="filter:url(#dropshadow)"
stroke-linejoin="round"
/>
<path
d="
M110 305 110 495 M220 305 220 495 M330 305 330 495 M440 305 440 495
M550 305 550 495 M660 305 660 495 M770 305 770 495 M880 305 880 495
M990 305 990 495 M1100 305 1100 495 M1210 305 1210 495 M1320 305 1320 495
M1430 305 1430 495 M1540 305 1540 495 M1650 305 1650 495 M1760 305 1760 495
M1858 305 1858 495
"
stroke-width="10"
stroke="rgb(122, 117, 113)"
/>
<rect class="string" x="0" y="324.3" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="353.6" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="382.9" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="412.2" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="441.5" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="470.8" width="1864" height="5" fill="#ccc" />
<polygon points="1863,324.3 1980,290 1980,295 1863,329.3" fill="#ccc" />
<polygon points="1863,353.6 2065,330 2065,335 1863,358.6" fill="#ccc" />
<polygon points="1863,382.9 2150,365 2150,370 1863,387.9" fill="#ccc" />
<polygon points="1863,412.2 2150,445 2150,450 1863,417.2" fill="#ccc" />
<polygon points="1863,441.5 2065,475 2065,480 1863,446.5" fill="#ccc" />
<polygon points="1863,470.8 1980,505 1980,510 1863,475.8" fill="#ccc" />
<circle cx="1980" cy="510" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2065" cy="480" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2150" cy="445" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2150" cy="365" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2065" cy="330" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="1980" cy="290" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
</svg>
它看起来是这样的:
虽然算不上最漂亮的吉他,但无论如何我都爱不释手!现在,让我们用一些 JS 和 CSS 让它可以弹奏!
将吉他弦夹紧到位
对于熟悉吉他/乐理的朋友,我将使用的标准调E A d g h e
。这些音符是在没有按下琴格时发出的。每按下一个琴格,这些音符的音高就会增加半个音符,所以对于一弦来说,应该是这样的:
E2 > F2 > Gb2 > G2 > Ab2 > A2 > Bb2 > H2 > C3 > Db3 > D3 Eb3 > E3 > ...
一旦出现环绕音,八度就会增加一个,然后循环重新开始。在朋友们的帮助下,我绘制了这张音符图:
const noteMap = [
['Ab3', 'G3 ', 'Gb3', 'F3 ', 'E3 ', 'Eb3', 'D3 ', 'Db3', 'C3 ', 'B2 ', 'Bb2', 'A2 ', 'Ab2', 'G2 ', 'Gb2', 'F2 ', 'E2 '],
['Db4', 'C4 ', 'B3 ', 'Bb3', 'A3 ', 'Ab3', 'G3 ', 'Gb3', 'F3 ', 'E3 ', 'Eb3', 'D3 ', 'Db3', 'C3 ', 'B2 ', 'Bb2', 'A2 '],
['Gb4', 'F4 ', 'E4 ', 'Eb4', 'D4 ', 'Db4', 'C4 ', 'B3 ', 'Bb3', 'A3 ', 'Ab3', 'G3 ', 'Gb3', 'F3 ', 'E3 ', 'Eb3', 'D3 '],
['B4 ', 'Bb4', 'A4 ', 'Ab4', 'G4 ', 'Gb4', 'F4 ', 'E4 ', 'Eb4', 'D4 ', 'Db4', 'C4 ', 'B3 ', 'Bb3', 'A3 ', 'Ab3', 'G3 '],
['Eb5', 'D5 ', 'Db5', 'C5 ', 'B4 ', 'Bb4', 'A4 ', 'Ab4', 'G4 ', 'Gb3', 'F4 ', 'E4 ', 'Eb4', 'D4 ', 'Db4', 'C4 ', 'B3 '],
['Ab5', 'G5 ', 'Gb5', 'F5 ', 'E5 ', 'Eb5', 'D5 ', 'Db5', 'C5 ', 'B4 ', 'Bb4', 'A4 ', 'Ab4', 'G4 ', 'Gb4', 'F4 ', 'E4 ']
]
(请注意,我在这里从右到左,因为最低的音符靠近头部。)
现在我需要让琴弦可点击。理想情况下,我会在每根琴弦的每个品位上添加可点击区域,以便确定琴弦被拨动的位置,从而确定要弹奏的音符。我使用 JS 动态地将它们添加到 SVG 中来实现这一点。我还添加了一个全局标志,isPlaying
用于判断鼠标是否被按下。该playNote()
函数当前输出的是将要弹奏的音符。
let isPlaying = false
function playNote (stringKey, note, force = false) {
if (isPlaying || force) {
console.log(note)
}
}
window.addEventListener('mousedown', () => {
isPlaying = true
})
window.addEventListener('mouseup', () => {
isPlaying = false
})
const svg = document.querySelector('#guitar')
noteMap.forEach((string, stringKey) => {
string.forEach((note, noteKey) => {
const area = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
area.setAttribute('x', noteKey * 110)
area.setAttribute('y', 315 + (29.3 * stringKey))
area.setAttribute('width', 110)
area.setAttribute('height', 20)
area.setAttribute('fill', '#fff')
area.setAttribute('opacity', '0')
area.addEventListener('click', () => {
playNote(stringKey, note, true)
})
area.addEventListener('mouseover', () => {
playNote(stringKey, note, false)
})
svg.appendChild(area)
})
})
让我们看看它的实际效果:
接下来,我为正在播放的琴弦添加一个持续三秒钟的动画,以便让用户直观地了解所选的琴弦:
const stringVibrationTimes = [0, 0, 0, 0, 0, 0]
const strings = Array.from(document.querySelectorAll('.string'))
setInterval(() => {
strings.forEach((stringEl, key) => {
if (stringVibrationTimes[key] > 0) {
stringEl.classList.add('vibrating')
} else {
stringEl.classList.remove('vibrating')
}
stringVibrationTimes[key] -= 50
if (stringVibrationTimes[key] < 0) {
stringVibrationTimes[key] = 0
}
})
}, 50)
function playNote (stringKey, note, force = false) {
if (isPlaying || force) {
console.log(note)
stringVibrationTimes[stringKey] = 3000
}
}
还有一些 CSS:
@keyframes vibrate {
0% {
transform: translateY(-2px);
}
50% {
transform: translateY(2px);
}
100% {
transform: translateY(-2px);
}
}
.string {
transform: translateY(0);
}
.string.vibrating {
animation: vibrate .05s infinite;
}
看起来棒极了:
我们已经走了一半了,现在只缺少声音!
把放大器调大!
为了让它播放声音,我使用了 Midi 声音字体。我会使用midi-js-soundfonts,因为我喜欢它的声音。我使用的乐器electric_guitar_clean
是FluidR3_GM
。我需要下载声音字体并将其放入名为 的文件夹中sound/
,以便浏览器可以使用它。要播放声音,我使用Audio
:
const soundFontUrl = './sound/'
function playNote (stringKey, note, force = false) {
if (isPlaying || force) {
console.log(note)
const audio = new Audio(soundFontUrl + note.trim() + '.mp3')
audio.play()
stringVibrationTimes[stringKey] = 3000
}
}
这是完整工作的演示 - 通过分别单击琴弦或按住鼠标并在琴弦上滑动来演奏:
编辑:抓住机会!
在评论中,devgrv建议添加一个选择作为光标 - 这正是我所做的,谢谢你的想法!
所以,我首先为吉他拨片创建了一个 SVG 文件。我在网上找了一个合适的形状,然后用路径和一些贝塞尔曲线重新绘制了它:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1280 1280" preserveAspectRatio="xMidYMid meet" width="80" height="80">
<defs>
<linearGradient id="pickbg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:rgb(77, 22, 22);" />
<stop offset="100%" style="stop-color:rgb(150, 47, 47);" />
</linearGradient>
</defs>
<g transform="rotate(135, 640, 640)">
<path
d="M120 310 C 330 -10 950 -10 1160 310 Q 980 1100 640 1210 Q 300 1100 120 310 Z"
fill="url(#pickbg)"
/>
</g>
</svg>
width
使用和属性将 SVG 缩小(最大 128 x 128)非常重要height
,因为浏览器会忽略所有更大的 SVG。接下来,我只需要将新的光标图像应用到 body 部分:
body {
/* ... */
cursor: url(./pick.svg), auto;
}
完成:
很好,一切准备就绪!
总结
这比Vue 的自制所见即所得 Markdown 编辑器还要好玩!玩起来真的很难,而且我确信 SVG 可以在某些地方进行优化,但它确实有效。如果你喜欢这篇文章,请告诉你的朋友,并大声说出来!
希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下❤️或🦄 !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。
如果您想支持我的努力, 请给我买杯咖啡☕ 或 在 Twitter 上关注我🐦 !
文章来源:https://dev.to/thormeier/let-s-build-an-actual-working-guitar-with-javascript-bfb