让我们用 JavaScript 💻🤘 打造一把真正能用的吉他🎸 锻造乐器 夹紧吉他弦 开启音箱! 编辑:拿起拨片! 总结

2025-05-28

让我们用 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>


Enter fullscreen mode Exit fullscreen mode

我使用多边形作为基本结构,使用矩形和多边形作为琴弦,使用路径作为音品:



<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>


Enter fullscreen mode Exit fullscreen mode

它看起来是这样的:

吉他琴颈和琴头的图像

虽然算不上最漂亮的吉他,但无论如何我都爱不释手!现在,让我们用一些 JS 和 CSS 让它可以弹奏!

将吉他弦夹紧到位

对于熟悉吉他/乐理的朋友,我将使用的标准调E A d g h e。这些音符是在没有按下琴格时发出的。每按下一个琴格,这些音符的音高就会增加半个音符,所以对于一弦来说,应该是这样的:



E2 > F2 > Gb2 > G2 > Ab2 > A2 > Bb2 > H2 > C3 > Db3 > D3 Eb3 > E3 > ...


Enter fullscreen mode Exit fullscreen mode

一旦出现环绕音,八度就会增加一个,然后循环重新开始。在朋友们的帮助下,我绘制了这张音符图:



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 ']
]


Enter fullscreen mode Exit fullscreen mode

(请注意,我在这里从右到左,因为最低的音符靠近头部。)

现在我需要让琴弦可点击。理想情况下,我会在每根琴弦的每个品位上添加可点击区域,以便确定琴弦被拨动的位置,从而确定要弹奏的音符。我使用 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)
  })
})


Enter fullscreen mode Exit fullscreen mode

让我们看看它的实际效果:

一张展示 SVG 吉他及其弦乐演奏的 gif 动图。

接下来,我为正在播放的琴弦添加一个持续三秒钟的动画,以便让用户直观地了解所选的琴弦:



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
  }
}


Enter fullscreen mode Exit fullscreen mode

还有一些 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;
}


Enter fullscreen mode Exit fullscreen mode

看起来棒极了:

振动吉他弦的特写,如上图所示

我们已经走了一半了,现在只缺少声音!

把放大器调大!

为了让它播放声音,我使用了 Midi 声音字体。我会使用midi-js-soundfonts,因为我喜欢它的声音。我使用的乐器electric_guitar_cleanFluidR3_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
  }
}


Enter fullscreen mode Exit fullscreen mode

这是完整工作的演示 - 通过分别单击琴弦或按住鼠标并在琴弦上滑动来演奏:

编辑:抓住机会!

在评论中,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>


Enter fullscreen mode Exit fullscreen mode

width使用和属性将 SVG 缩小(最大 128 x 128)非常重要height,因为浏览器会忽略所有更大的 SVG。接下来,我只需要将新的光标图像应用到 body 部分:



body {
  /* ... */
  cursor: url(./pick.svg), auto;
}


Enter fullscreen mode Exit fullscreen mode

完成:

带有拨片作为光标的吉他动图

很好,一切准备就绪!

总结

这比Vue 的自制所见即所得 Markdown 编辑器还要好玩!玩起来真的很难,而且我确信 SVG 可以在某些地方进行优化,但它确实有效。如果你喜欢这篇文章,请告诉你的朋友,并大声说出来


希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下❤️🦄 !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。

如果您想支持我的努力, 请给我买杯咖啡 在 Twitter 上关注我🐦

给我买个咖啡按钮

文章来源:https://dev.to/thormeier/let-s-build-an-actual-working-guitar-with-javascript-bfb
PREV
一年前自学编程,最近发布了我的第一款个人产品。我学习的最大秘诀……🧠
NEXT
在 dev.to 上写文章的一年半让我成为了一名更好的开发者✍️↔️🧑‍💻🚀