index.html 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <!doctype html>
  2. <html>
  3. <head>
  4. <title>Keyboard</title>
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <style>
  7. :root {
  8. --white-key-width: 60px;
  9. --black-key-width: 40px;
  10. --white-key-height: calc(5 * var(--white-key-width));
  11. --black-key-height: calc(3 * var(--white-key-width));
  12. font-family: 'Linux Libertine O', serif;
  13. }
  14. * {
  15. box-sizing: border-box;
  16. }
  17. html {
  18. height: 100%;
  19. overflow-x: hidden;
  20. }
  21. body {
  22. background: whitesmoke;
  23. display: flex;
  24. flex-direction: column;
  25. gap: 1em;
  26. height: 100%;
  27. justify-content: center;
  28. margin: 0px;
  29. padding-bottom: 4em;
  30. }
  31. main {
  32. position: relative;
  33. height: var(--white-key-height);
  34. margin-left: auto;
  35. margin-right: auto;
  36. }
  37. .keys {
  38. display: flex;
  39. position: absolute;
  40. top: 0em;
  41. pointer-events: none;
  42. }
  43. .keys.white {
  44. left: 0em;
  45. border-radius: 4px 4px 0px 0px;
  46. border-top: 2px solid #999;
  47. overflow: hidden;
  48. }
  49. .keys.black {
  50. left: calc(var(--white-key-width) - var(--black-key-width) / 2);
  51. }
  52. .key {
  53. position: relative;
  54. font-size: 100%;
  55. pointer-events: all;
  56. border-radius: 0px 0px 4px 4px;
  57. }
  58. .key.white {
  59. height: var(--white-key-height);
  60. width: var(--white-key-width);
  61. border: 2px solid #999;
  62. border-top: none;
  63. border-left: none;
  64. background: white;
  65. box-shadow: inset 0px 0px 5px 3px rgba(0, 0, 0, 0.15);
  66. }
  67. .key.white:first-child {
  68. border-left: 2px solid #999;
  69. }
  70. .key.white:hover, .key.white:focus {
  71. background: #EEE;
  72. }
  73. .key.white:active {
  74. background: linear-gradient(#EEE, #DDD);
  75. box-shadow: inset 0px 0px 5px 3px rgba(0, 0, 0, 0.25);
  76. }
  77. .key.black {
  78. height: var(--black-key-height);
  79. width: var(--black-key-width);
  80. background: #333;
  81. color: white;
  82. border: 2px solid #444;
  83. box-shadow: inset 0px -4px 4px 5px rgba(255, 255, 255, 0.2);
  84. }
  85. .key.black:hover, .key.black:focus {
  86. background: #444;
  87. }
  88. .key.black:active {
  89. background: linear-gradient(#444, #222);
  90. box-shadow: inset 0px -4px 4px 5px rgba(255, 255, 255, 0.0);
  91. }
  92. .key.black + .key.black {
  93. margin-left: calc(var(--white-key-width) - var(--black-key-width));
  94. }
  95. .key .label {
  96. position: absolute;
  97. bottom: 0.5em;
  98. width: 100%;
  99. text-align: center;
  100. }
  101. .phony {
  102. visibility: hidden;
  103. pointer-events: none;
  104. }
  105. #target {
  106. font-size: 300%;
  107. display: inline-block;
  108. padding: 0rem 1rem;
  109. vertical-align: middle;
  110. }
  111. #results {
  112. margin-left: 1em;
  113. display: inline-block;
  114. overflow-x: scroll;
  115. padding: 0rem 1rem;
  116. vertical-align: middle;
  117. }
  118. .answer {
  119. font-size: 300%;
  120. }
  121. .answer.correct {
  122. color: green;
  123. }
  124. .answer.incorrect {
  125. color: red;
  126. }
  127. .top-panel {
  128. display: flex;
  129. margin-left: auto;
  130. margin-right: auto;
  131. }
  132. .options {
  133. display: flex;
  134. align-items: center;
  135. justify-content: center;
  136. gap: 2em;
  137. }
  138. .options > div {
  139. display: flex;
  140. align-items: center;
  141. gap: 1ch;
  142. }
  143. .octave-2, .octave-3, .octave-4 {
  144. display: none;
  145. }
  146. main, .top-panel {
  147. width: calc(var(--white-key-width) * 7);
  148. }
  149. @media only screen and (min-width: 850px) {
  150. .octave-2 { display: block; }
  151. main, .top-panel {
  152. width: calc(var(--white-key-width) * 14);
  153. }
  154. }
  155. @media only screen and (min-width: 1260px) {
  156. .octave-2, .octave-3 { display: unset; }
  157. main, .top-panel {
  158. width: calc(var(--white-key-width) * 21);
  159. }
  160. }
  161. </style>
  162. </head>
  163. <body>
  164. <header class="top-panel">
  165. <span id="target"></span>
  166. <span id="results"></span>
  167. </header>
  168. <main>
  169. <div class="white keys"></div>
  170. <div class="black keys"></div>
  171. </main>
  172. <footer class="options">
  173. <div>
  174. <label for="level">Difficulty</label>
  175. <input type="range" id="level" min="1" max="95" />
  176. </div>
  177. <div>
  178. <input type="checkbox" id="show-labels" onchange="toggleLabels()"/>
  179. <label for="show-labels">Show labels</label>
  180. </div>
  181. </footer>
  182. <script>
  183. const notes = 'CDEFGAB';
  184. function main() {
  185. createKeyboard();
  186. setTarget();
  187. }
  188. function setTarget() {
  189. const difficulty = Number(document.getElementById('level').value);
  190. const maxNote = Math.floor(difficulty / 5) + 2;
  191. document.getElementById('target').innerHTML = randomChoice(
  192. [
  193. 'C', 'F', 'A', 'D', 'B', 'G', 'E',
  194. 'C♯', 'F♯', 'A♭', 'A♯', 'D♭', 'D♯', 'B♭', 'G♯', 'G♭', 'E♭',
  195. 'C♭', 'B♯', 'F♭', 'E♯',
  196. ].slice(0, maxNote)
  197. );
  198. }
  199. function createKeyboard() {
  200. const whiteKeyCount = 28;
  201. const keyboard = document.querySelector('main');
  202. const whiteKeys = document.querySelector('main > .white.keys');
  203. const blackKeys = document.querySelector('main > .black.keys');
  204. let octave = 0;
  205. for (let i = 0; i < whiteKeyCount; i++){
  206. if (i % 7 == 0) {
  207. octave += 1;
  208. }
  209. const note = notes[i % 7];
  210. const key = document.createElement('button');
  211. key.className = `key white ${note} octave-${octave}`;
  212. key.setAttribute('tabindex', 100 + i * 2);
  213. const label = document.createElement('div');
  214. label.className = 'label';
  215. label.innerHTML = (
  216. note == 'C' ? 'B♯<br>' :
  217. note == 'B' ? 'C♭<br>' :
  218. note == 'F' ? 'E♯<br>' :
  219. note == 'E' ? 'F♭<br>' : ''
  220. ) + note;
  221. key.append(label);
  222. whiteKeys.appendChild(key);
  223. }
  224. octave = 0;
  225. for (let i = 0; i < whiteKeyCount - 1; i++){
  226. if (i % 7 == 0) {
  227. octave += 1;
  228. }
  229. const key = document.createElement('button');
  230. key.className = `key black`;
  231. key.setAttribute('tabindex', 100 + i * 2 + 1);
  232. if (i % 7 == 2 || i % 7 == 6) {
  233. key.className += ' phony';
  234. } else {
  235. key.className += ` ${notes[i % 7]}-sharp ${notes[(i + 1) % 7]}-flat octave-${octave}`;
  236. const label = document.createElement('div');
  237. label.className = 'label';
  238. label.innerHTML = `${notes[i % 7]}♯ ${notes[(i + 1) % 7]}♭`;
  239. key.appendChild(label);
  240. }
  241. blackKeys.appendChild(key);
  242. }
  243. for (const key of document.querySelectorAll('.key')) {
  244. key.addEventListener('click', (evt) => {
  245. const note = document.getElementById('target').innerHTML;
  246. const noteClass = note.replace('♭', '-flat').replace('♯', '-sharp');
  247. const correct = key.classList.contains(noteClass);
  248. if (!document.getElementById('show-labels').checked) {
  249. document.getElementById('level').value = Number(
  250. document.getElementById('level').value
  251. ) + (correct ? 1 : -1);
  252. }
  253. document.getElementById('results').innerHTML =
  254. (correct
  255. ? `<span class="answer correct">${note}</span>`
  256. : `<span class="answer incorrect">${note}</span>`
  257. ) + document.getElementById('results').innerHTML
  258. setTarget();
  259. });
  260. }
  261. toggleLabels();
  262. }
  263. function toggleLabels() {
  264. const checked = document.getElementById('show-labels').checked;
  265. for (const label of document.querySelectorAll('.label')) {
  266. label.style.visibility = checked ? 'visible' : 'hidden';
  267. }
  268. }
  269. var randomChoice = (arr) => arr[Math.floor(Math.random() * arr.length)];
  270. main();
  271. </script>
  272. </body>
  273. </html>