こちらのページで電子ピアノの鍵盤の配置を変えた際、hover (鍵盤の上にマウスを移動させたときに色が変わる) と active (鍵盤を押したときに色が変わる) の効果を外しました。HTML の子要素である黒鍵に対する hover と active の効果を、親要素である白鍵の hover と active の効果から切り離すことができなかったためです。
こちらのページを参考に、白鍵と黒鍵の関係を親子関係ではなく sibling (兄弟姉妹) の関係にして、白鍵と黒鍵の hover と active 効果を切り離しました。
1. JavaScript の主な変更点 (鍵盤の HTML を生成するコードを変更)
下記のコードのように class 属性が key-set-parent の div 要素の子要素として、白鍵と黒鍵のペアを用意することにしました。ピアノの鍵盤を左の鍵盤から順に配置していき、白鍵の右に黒鍵があるド、レ、ファ、ソ、ラの白鍵のときだけ白鍵と黒鍵の両方を class 属性が key-set-parent の div 要素の子要素として追加します。ミ、シのときは、白鍵のみ class 属性が key-set-parent の div 要素の子要素として追加します。
for (let i = 0; i < keyList.length; i++) { const keySetElem = document.createElement("div"); keySetElem.className = "key-set-parent"; const whiteKey = keyList[i]; const whiteKeyName = whiteKey[0]; const whiteKeyElem = createKey(whiteKeyName, idx, whiteKey[1], 'white-key'); keySetElem.appendChild(whiteKeyElem); if ( whiteKeyName === 'ド' || whiteKeyName === 'レ' || whiteKeyName === 'ファ' || whiteKeyName === 'ソ' || whiteKeyName === 'ラ') { const blackKey = keyList[++i]; if (blackKey != undefined) { const blackKeyName = blackKey[0]; const blackKeyElem = createKey(blackKeyName, idx, blackKey[1], 'black-key'); keySetElem.appendChild(blackKeyElem); } } octaveElem.appendChild(keySetElem); } keyboard.appendChild(octaveElem); });
2. cssの主な変更点
下記のように白鍵と黒鍵のペアを格納する親要素の class セレクター key-set-parent を用意します。display プロパティで inline-block を指定し、指定したサイズのHTML要素が水平方向に配置されるようにします。key-set-parent のサイズを指定する width と height は白鍵のサイズと同じです。
.key-set-parent { position: relative; display: inline-block; width: 40px; height: 120px; }
3. 修正後のコード全体
3.1. css
<style type="text/css"> .container { overflow-x: scroll; overflow-y: hidden; width: 100%; height: 180px; white-space: nowrap; margin: 10px; } .keyboard { width: auto; padding: 0; margin: 0; } .key-set-parent { position: relative; display: inline-block; width: 40px; height: 120px; } .key { position: relative; cursor: pointer; font: 10px "Open Sans", "Lucida Grande", "Arial", sans-serif; border: 1px solid black; border-radius: 5px; width: 40px; height: 120px; text-align: center; box-shadow: 2px 2px darkgray; display: inline-block; margin-right: 3px; user-select: none; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; } .key.black-key { position: absolute; background-color: #000; color: #fff; width: 36px; height: 80px; top: 0px; left: 22px; z-index: 1; pointer-events: auto; vertical-align: top; } .key div { position: absolute; bottom: 0; text-align: center; width: 100%; pointer-events: none; } .key div sub { font-size: 8px; pointer-events: none; } .key:hover { background-color: #eef; } .key.black-key:hover { background-color: #778; } .key:active { background-color: #000; color: #fff; } .key.black-key:active { background-color: #fff; color: #000; } .octave { display: inline-block; padding: 0 6px 0 0; } .settingsBar { padding-top: 8px; font: 14px "Open Sans", "Lucida Grande", "Arial", sans-serif; position: relative; vertical-align: middle; width: 100%; height: 60px; } .left { width: 50%; position: absolute; left: 0; display: table-cell; vertical-align: middle; } .left span, .left input { vertical-align: middle; } .right { width: 50%; position: absolute; right: 0; display: table-cell; vertical-align: middle; } .right span { vertical-align: middle; } .right input { vertical-align: baseline; } </style>
3.2. HTML
<div class="container"> <div class="keyboard"></div> </div> <div class="settingsBar"> <div class="left"> <span>Volume: </span> <input type="range" min="0.0" max="1.0" step="0.01" value="0.5" list="volumes" name="volume"> <datalist id="volumes"> <option value="0.0" label="Mute"> <option value="1.0" label="100%"> </datalist> </div> <div class="right"> <span>Current waveform: </span> <select name="waveform"> <option value="sine">Sine</option> <option value="square">Square</option> <option value="sawtooth">Sawtooth</option> <option value="triangle" selected>Triangle</option> <option value="custom">Custom</option> </select> </div> </div>
3.3. JavaScript
<script> const audioContext = new (window.AudioContext || window.webkitAudioContext)(); let oscList = []; const keyboard = document.querySelector(".keyboard"); const wavePicker = document.querySelector("select[name='waveform']"); const volumeControl = document.querySelector("input[name='volume']"); let noteFreq = null; let customWaveform = null; let sineTerms = null; let cosineTerms = null; const note_names = [ ["ラ", "", "A", ""], ["ラ#", "シ$\\flat$", "A#", "B$\\flat$"], ["シ", "","B", ""], ["ド", "","C", ""], ["ド#", "レ$\\flat$","C#", "D$\\flat$"], ["レ", "","D", ""], ["レ#", "ミ$\\flat$","D#", "E$\\flat$"], ["ミ", "","E", ""], ["ファ", "","F", ""], ["ファ#", "ソ$\\flat$","F#", "G$\\flat$"], ["ソ", "", "G", ""], ["ソ#", "ラ$\\flat$", "G#", "A$\\flat$"] ]; setup(); // ------------------------------------------------------- // functions // ------------------------------------------------------- function createNoteTable() { let noteFreq = []; for (let octave = 0; octave < 9; octave++) { noteFreq[octave] = []; } for (let n = 0; n < 88; n++) { const frequency = getAudioFrequency(n); let octave = parseInt(n/12); if (n % 12 >= 3) { octave++; } const note_name_sharp_japanese = note_names[n % 12][0]; noteFreq[octave][note_name_sharp_japanese] = frequency; } return noteFreq; } function getAudioFrequency(n) { return 27.5 * ( Math.pow( Math.pow(2, 1/12), n) ); } function setup() { noteFreq = createNoteTable(); noteFreq.forEach(function(keys, idx) { const keyList = Object.entries(keys); const octaveElem = document.createElement("div"); octaveElem.className = "octave"; for (let i = 0; i < keyList.length; i++) { const keySetElem = document.createElement("div"); keySetElem.className = "key-set-parent"; const whiteKey = keyList[i]; const whiteKeyName = whiteKey[0]; const whiteKeyElem = createKey(whiteKeyName, idx, whiteKey[1], 'white-key'); keySetElem.appendChild(whiteKeyElem); if ( whiteKeyName === 'ド' || whiteKeyName === 'レ' || whiteKeyName === 'ファ' || whiteKeyName === 'ソ' || whiteKeyName === 'ラ') { const blackKey = keyList[++i]; if (blackKey != undefined) { const blackKeyName = blackKey[0]; const blackKeyElem = createKey(blackKeyName, idx, blackKey[1], 'black-key'); keySetElem.appendChild(blackKeyElem); } } octaveElem.appendChild(keySetElem); } keyboard.appendChild(octaveElem); }); document.querySelector("div[data-note='ファ'][data-octave='5']").scrollIntoView(false); sineTerms = new Float32Array([0, 0, 1, 0, 1]); cosineTerms = new Float32Array(sineTerms.length); customWaveform = audioContext.createPeriodicWave(cosineTerms, sineTerms); for (i = 0; i < 9; i++) { oscList[i] = {}; } } function createKey(note, octave, freq, keyColor) { const keyElement = document.createElement("div"); const labelElement = document.createElement("div"); if (keyColor === 'black-key') { keyElement.className = "key black-key"; } else { keyElement.className = "key"; } keyElement.dataset["octave"] = octave; keyElement.dataset["note"] = note; keyElement.dataset["frequency"] = freq; labelElement.innerHTML = note + "<sub>" + octave + "</sub>"; keyElement.appendChild(labelElement); keyElement.addEventListener("mousedown", notePressed, false); keyElement.addEventListener("touchstart", notePressed, false); return keyElement; } function playTone(freq) { const osc = audioContext.createOscillator(); const volume = volumeControl.value; const gainNode = audioContext.createGain(); gainNode.connect(audioContext.destination); gainNode.gain.setValueAtTime(0, audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.05); gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1.00); osc.connect(gainNode); const type = wavePicker.options[wavePicker.selectedIndex].value; if (type == "custom") { osc.setPeriodicWave(customWaveform); } else { osc.type = type; } osc.frequency.value = freq; osc.start(); osc.stop(audioContext.currentTime + 1.00); return osc; } function notePressed(event) { const dataset = event.target.dataset; const octave = +dataset["octave"]; oscList[octave][dataset["note"]] = playTone(dataset["frequency"]); } </script>