Skip to content

Instantly share code, notes, and snippets.

@peerreynders
Last active March 30, 2019 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peerreynders/c44d3bd373340d0f21fbe50435e41108 to your computer and use it in GitHub Desktop.
Save peerreynders/c44d3bd373340d0f21fbe50435e41108 to your computer and use it in GitHub Desktop.
Web Components in Action v6: 5.2.1 Creating a Musical Instrument with Web Components and JS Modules
<html>
<head>
<!--
file: index.html
https://livebook.manning.com/#!/book/web-components-in-action/chapter-5/v-6/comment-487548
https://github.com/bengfarrell/webcomponentsinaction
-->
<title>Web Harp</title>
<link href="./main.css" type="text/css" rel="stylesheet">
<link href="./csshake.min.css" type="text/css" rel="stylesheet">
</head>
<body>
<button>start</button>
<!-- https://cdn.jsdelivr.net/npm/midi.js@0.3.1/lib/midi.min.js -->
<script src="./midi.min.js"></script>
<script type="module">
import _dontCare from './components/webharp-app.mjs'
function startWebHarp(_event) {
let element = document.createElement('webharp-app')
element.setAttribute('strings','10')
document.body.removeChild(button)
document.body.appendChild(element)
}
const button = document.querySelector('button')
button.addEventListener('click', startWebHarp, {once: true})
</script>
</body>
</html>
// file: helpers/midiFacade.mjs
var promise
let loaded = false
function toLoadedState(result) {
loaded = result
}
/*
https://raw.githubusercontent.com/mudcube/MIDI.js/master/examples/soundfont/acoustic_grand_piano-ogg.js
*/
function executor(resolve, _reject) {
const onsuccess = () => {
resolve(true)
}
MIDI.loadPlugin({
soundfontUrl: './',
instrument: 'acoustic_grand_piano',
onsuccess
})
}
export function loadPlugin() {
if (!promise) {
promise = new Promise(executor)
promise.then(toLoadedState)
}
return promise
}
export function isReady() {
return loaded
}
export function setVolume(...args) {
MIDI.setVolume(...args)
}
export function noteOn(...args) {
MIDI.noteOn(...args)
}
export function noteOff(...args) {
MIDI.noteOff(...args)
}
// file: components/webharp-strings.mjs
import _dontCare from './webharp-strings.mjs'
function pointFromEvent({pageX, pageY}) {
return {x: pageX, y: pageY}
}
export default class WebHarpApp extends HTMLElement {
constructor() {
super()
this._onMouseMove = this._onMouseMove.bind(this)
}
connectedCallback() {
this.innerHTML =
`<webharp-strings
strings="${this.getAttribute('strings')}">
</webharp-strings>`
this._stringsElement = this.querySelector('webharp-strings')
this.addEventListener('mousemove', this._onMouseMove)
}
_onMouseMove(event){
this._stringsElement.point = pointFromEvent(event)
}
}
if (!customElements.get('webharp-app')) {
customElements.define('webharp-app', WebHarpApp)
}
// file: components/webharp-string.js
import * as midi from '../helpers/midiFacade.mjs'
const STRUM_BASE = ['shake', 'shake-constant']
const STRUM_LITTLE = [...STRUM_BASE, 'shake-little']
const STRUM_NORMAL = [...STRUM_BASE, 'shake-horizontal']
const STRUM_REMOVE = [...new Set([...STRUM_LITTLE, ...STRUM_NORMAL])]
export default class WebHarpString extends HTMLElement {
constructor() {
super()
this._stopStrum = this._stopStrum.bind(this)
}
connectedCallback() {
this.innerHTML =
`<div class="line"></div>
<style>
webharp-string > .line {
background-color: white;
height: 100%;
width: 2px;
}
</style>`
}
strum(position, power) {
if (this._timer) {
clearTimeout(this._timer)
this._timer = null
}
const dur = power * 10 + 250
const classNames = dur < 500 ? STRUM_LITTLE : STRUM_NORMAL
this.classList.add(...classNames)
this._timer = setTimeout(this._stopStrum, dur)
this._playSound(position, power)
}
_stopStrum() {
this.classList.remove(...STRUM_REMOVE)
}
_playSound(position, power) {
if (!midi.isReady()) {
const playSound = _result => {
this._playSound(position, power)
}
midi.loadPlugin().then(playSound)
return
}
const note = 60 + position * 5
midi.setVolume(0, 127)
midi.noteOn(0, note, power, 0)
midi.noteOff(0, note, 0.75)
}
}
if (!customElements.get('webharp-string')) {
customElements.define('webharp-string', WebHarpString)
}
import _dontCare from './webharp-string.mjs'
function renderStrings(count) {
return '<webharp-string></webharp-string>'.repeat(count)
}
export default class WebHarpStrings extends HTMLElement {
set point(current) {
if(!this._stringElements) {
return
}
const last = this._point
this._point = current
if (!last || !current) {
return
}
const [xMin, xMax] = current.x > last.x ?
[last.x, current.x] :
[current.x, last.x]
const magnitude = xMax - xMin
const strumTouched = (element, index) => {
if (xMin <= element.offsetLeft && element.offsetLeft <= xMax) {
element.strum(index, magnitude)
}
}
this._stringElements.forEach(strumTouched)
}
connectedCallback() {
this.innerHTML =
`<div class="spacer"></div>
${renderStrings(this._getStringsAttribute())}
<style>
webharp-strings {
width: 100%
height: 100%;
display: flex;
}
webharp-strings > webharp-string, div.spacer {
flex: 1;
}
</style>`
this._stringElements = this.querySelectorAll('webharp-string')
}
_getStringsAttribute() {
const minCount = 1
const value = this.hasAttribute('strings') ?
this.getAttribute('strings') :
''
const number = value || minCount
return number > minCount ? number : minCount
}
}
if (!customElements.get('webharp-strings')) {
customElements.define('webharp-strings', WebHarpStrings)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment