こちらのページの電子ピアノの鍵盤の配置を変えました。
Volume:
Current waveform:
主な変更点は HTML を生成する下記の JavaScript で白鍵の子要素として黒鍵を用意し、css で黒鍵の子要素の position に absolute を指定した点です。黒鍵の位置を指定する css で親要素の白鍵の位置に対する黒鍵の相対的な位置を top と left で指定しています。黒鍵が白鍵の前面に表示されるように z-index: 1; を指定しています。また、黒鍵を押したときに親要素の白鍵ではなく黒鍵の音が鳴るように pointer-events: auto; を指定しています。
1. JavaScript の主な変更点 (鍵盤のHTMLを生成するコードを変更)
noteFreq.forEach(function(keys, idx) { const keyList = Object.entries(keys); const octaveElem = document.createElement("div"); octaveElem.className = "octave"; let whiteKey = null; let blackKey = null; keyList.forEach(function(key) { const key_name = key[0]; if (key_name === 'ド' || key_name === 'レ' || key_name === 'ミ' || key_name === 'ファ' || key_name === 'ソ' || key_name === 'ラ' || key_name === 'シ') { if (whiteKey != null) { octaveElem.appendChild(whiteKey); } whiteKey = createKey(key_name, idx, key[1], 'white-key'); } else { blackKey = createKey(key_name, idx, key[1], 'black-key'); whiteKey.appendChild(blackKey); } }); octaveElem.appendChild(whiteKey); keyboard.appendChild(octaveElem); });
2. cssの主な変更点
.key.black-key { position: absolute; background-color: #000; color: #fff; height: 80px; top: 0px; left: 20px; z-index: 1; pointer-events: auto; vertical-align: top; }
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 { position: relative; cursor: pointer; font: 12px "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; height: 80px; top: 0px; left: 20px; 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: 10px; pointer-events: none; } .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"; let whiteKey = null; let blackKey = null; keyList.forEach(function(key) { const key_name = key[0]; if (key_name === 'ド' || key_name === 'レ' || key_name === 'ミ' || key_name === 'ファ' || key_name === 'ソ' || key_name === 'ラ' || key_name === 'シ') { if (whiteKey != null) { octaveElem.appendChild(whiteKey); } whiteKey = createKey(key_name, idx, key[1], 'white-key'); } else { blackKey = createKey(key_name, idx, key[1], 'black-key'); whiteKey.appendChild(blackKey); } }); octaveElem.appendChild(whiteKey); 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>