123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- <!doctype html>
- <html>
- <head>
- <title>Keyboard</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- :root {
- --white-key-width: 60px;
- --black-key-width: 40px;
- --white-key-height: calc(5 * var(--white-key-width));
- --black-key-height: calc(3 * var(--white-key-width));
- font-family: 'Linux Libertine O', serif;
- }
- * {
- box-sizing: border-box;
- }
- html {
- height: 100%;
- overflow-x: hidden;
- }
- body {
- background: whitesmoke;
- display: flex;
- flex-direction: column;
- gap: 1em;
- height: 100%;
- justify-content: center;
- margin: 0px;
- padding-bottom: 4em;
- }
- main {
- position: relative;
- height: var(--white-key-height);
- margin-left: auto;
- margin-right: auto;
- }
- .keys {
- display: flex;
- position: absolute;
- top: 0em;
- pointer-events: none;
- }
- .keys.white {
- left: 0em;
- border-radius: 4px 4px 0px 0px;
- border-top: 2px solid #999;
- overflow: hidden;
- }
- .keys.black {
- left: calc(var(--white-key-width) - var(--black-key-width) / 2);
- }
- .key {
- position: relative;
- font-size: 100%;
- pointer-events: all;
- border-radius: 0px 0px 4px 4px;
- }
- .key.white {
- height: var(--white-key-height);
- width: var(--white-key-width);
- border: 2px solid #999;
- border-top: none;
- border-left: none;
- background: white;
- box-shadow: inset 0px 0px 5px 3px rgba(0, 0, 0, 0.15);
- }
- .key.white:first-child {
- border-left: 2px solid #999;
- }
- .key.white:hover, .key.white:focus {
- background: #EEE;
- }
- .key.white:active {
- background: linear-gradient(#EEE, #DDD);
- box-shadow: inset 0px 0px 5px 3px rgba(0, 0, 0, 0.25);
- }
- .key.black {
- height: var(--black-key-height);
- width: var(--black-key-width);
- background: #333;
- color: white;
- border: 2px solid #444;
- box-shadow: inset 0px -4px 4px 5px rgba(255, 255, 255, 0.2);
- }
- .key.black:hover, .key.black:focus {
- background: #444;
- }
- .key.black:active {
- background: linear-gradient(#444, #222);
- box-shadow: inset 0px -4px 4px 5px rgba(255, 255, 255, 0.0);
- }
- .key.black + .key.black {
- margin-left: calc(var(--white-key-width) - var(--black-key-width));
- }
- .key .label {
- position: absolute;
- bottom: 0.5em;
- width: 100%;
- text-align: center;
- }
- .phony {
- visibility: hidden;
- pointer-events: none;
- }
- #target {
- font-size: 300%;
- display: inline-block;
- padding: 0rem 1rem;
- vertical-align: middle;
- }
- #results {
- margin-left: 1em;
- display: inline-block;
- overflow-x: scroll;
- padding: 0rem 1rem;
- vertical-align: middle;
- }
- .answer {
- font-size: 300%;
- }
- .answer.correct {
- color: green;
- }
- .answer.incorrect {
- color: red;
- }
- .top-panel {
- display: flex;
- margin-left: auto;
- margin-right: auto;
- }
- .options {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 2em;
- }
- .options > div {
- display: flex;
- align-items: center;
- gap: 1ch;
- }
- .octave-2, .octave-3, .octave-4 {
- display: none;
- }
- main, .top-panel {
- width: calc(var(--white-key-width) * 7);
- }
- @media only screen and (min-width: 850px) {
- .octave-2 { display: block; }
- main, .top-panel {
- width: calc(var(--white-key-width) * 14);
- }
- }
- @media only screen and (min-width: 1260px) {
- .octave-2, .octave-3 { display: unset; }
- main, .top-panel {
- width: calc(var(--white-key-width) * 21);
- }
- }
- </style>
- </head>
- <body>
- <header class="top-panel">
- <span id="target"></span>
- <span id="results"></span>
- </header>
- <main>
- <div class="white keys"></div>
- <div class="black keys"></div>
- </main>
- <footer class="options">
- <div>
- <label for="level">Difficulty</label>
- <input type="range" id="level" min="1" max="95" />
- </div>
- <div>
- <input type="checkbox" id="show-labels" onchange="toggleLabels()"/>
- <label for="show-labels">Show labels</label>
- </div>
- </footer>
- <script>
- const notes = 'CDEFGAB';
- function main() {
- createKeyboard();
- setTarget();
- }
- function setTarget() {
- const difficulty = Number(document.getElementById('level').value);
- const maxNote = Math.floor(difficulty / 5) + 2;
- document.getElementById('target').innerHTML = randomChoice(
- [
- 'C', 'F', 'A', 'D', 'B', 'G', 'E',
- 'C♯', 'F♯', 'A♭', 'A♯', 'D♭', 'D♯', 'B♭', 'G♯', 'G♭', 'E♭',
- 'C♭', 'B♯', 'F♭', 'E♯',
- ].slice(0, maxNote)
- );
- }
- function createKeyboard() {
- const whiteKeyCount = 28;
- const keyboard = document.querySelector('main');
- const whiteKeys = document.querySelector('main > .white.keys');
- const blackKeys = document.querySelector('main > .black.keys');
- let octave = 0;
- for (let i = 0; i < whiteKeyCount; i++){
- if (i % 7 == 0) {
- octave += 1;
- }
- const note = notes[i % 7];
- const key = document.createElement('button');
- key.className = `key white ${note} octave-${octave}`;
- key.setAttribute('tabindex', 100 + i * 2);
- const label = document.createElement('div');
- label.className = 'label';
- label.innerHTML = (
- note == 'C' ? 'B♯<br>' :
- note == 'B' ? 'C♭<br>' :
- note == 'F' ? 'E♯<br>' :
- note == 'E' ? 'F♭<br>' : ''
- ) + note;
- key.append(label);
- whiteKeys.appendChild(key);
- }
- octave = 0;
- for (let i = 0; i < whiteKeyCount - 1; i++){
- if (i % 7 == 0) {
- octave += 1;
- }
- const key = document.createElement('button');
- key.className = `key black`;
- key.setAttribute('tabindex', 100 + i * 2 + 1);
- if (i % 7 == 2 || i % 7 == 6) {
- key.className += ' phony';
- } else {
- key.className += ` ${notes[i % 7]}-sharp ${notes[(i + 1) % 7]}-flat octave-${octave}`;
- const label = document.createElement('div');
- label.className = 'label';
- label.innerHTML = `${notes[i % 7]}♯ ${notes[(i + 1) % 7]}♭`;
- key.appendChild(label);
- }
- blackKeys.appendChild(key);
- }
- for (const key of document.querySelectorAll('.key')) {
- key.addEventListener('click', (evt) => {
- const note = document.getElementById('target').innerHTML;
- const noteClass = note.replace('♭', '-flat').replace('♯', '-sharp');
- const correct = key.classList.contains(noteClass);
- if (!document.getElementById('show-labels').checked) {
- document.getElementById('level').value = Number(
- document.getElementById('level').value
- ) + (correct ? 1 : -1);
- }
- document.getElementById('results').innerHTML =
- (correct
- ? `<span class="answer correct">${note}</span>`
- : `<span class="answer incorrect">${note}</span>`
- ) + document.getElementById('results').innerHTML
- setTarget();
- });
- }
- toggleLabels();
- }
- function toggleLabels() {
- const checked = document.getElementById('show-labels').checked;
- for (const label of document.querySelectorAll('.label')) {
- label.style.visibility = checked ? 'visible' : 'hidden';
- }
- }
- var randomChoice = (arr) => arr[Math.floor(Math.random() * arr.length)];
- main();
- </script>
- </body>
- </html>
|