1let morse = new Morse(100, update_display); 2let audio = new MorseAudio(morse, document.getElementById("key")); 3audio.on_play_ends.push(on_audio_off); 4 5let history = new SessionHistory(); 6history.render(); 7history.bind_reset_button(document.getElementById("history-reset")); 8 9let last_played_idx = 0; 10audio.on_play_letters.push(idx => last_played_idx = idx); 11 12bind_speed_input( 13 morse, 14 document.getElementById("split-speed-cb"), 15 document.getElementById("speed-wpm"), 16 document.getElementById("speed-wpm-char"), 17 document.getElementById("speed-wpm-word"), 18); 19 20bind_volume_input(audio, document.getElementById("volume")); 21bind_enable_light(audio, document.getElementById("enable-light")); 22bind_frequency_input(audio, document.getElementById("freq")); 23 24let settings_screen = document.getElementById("settings"); 25document.getElementById("settings-switch").addEventListener("click", e => { 26 e.preventDefault(); 27 settings_screen.classList.toggle("active"); 28}); 29 30document.getElementById("test-tone").addEventListener("mousedown", e => { 31 audio.init_user(); 32 audio.on(); 33}); 34 35document.getElementById("test-tone").addEventListener("mouseup", e => { 36 audio.init_user(); 37 audio.off(); 38}); 39 40const MODE_WORD = 'word'; 41const MODE_WORD_EL = document.getElementById("button-select-word"); 42 43const MODE_SENTENCE = 'sentence'; 44const MODE_SENTENCE_EL = document.getElementById("button-select-sentence"); 45 46var current_mode = null; 47 48function set_mode(new_mode) { 49 current_mode = new_mode; 50 localStorage.setItem("mode", current_mode); 51 MODE_WORD_EL.classList.remove("selected"); 52 MODE_SENTENCE_EL.classList.remove("selected"); 53 if (new_mode === MODE_WORD) { 54 MODE_WORD_EL.classList.add("selected"); 55 } else if (new_mode === MODE_SENTENCE) { 56 MODE_SENTENCE_EL.classList.add("selected"); 57 } else { 58 console.error("Unknown mode: " + new_mode); 59 } 60} 61set_mode(localStorage.getItem("mode") || MODE_SENTENCE); 62MODE_WORD_EL.addEventListener("click", e => set_mode(MODE_WORD)); 63MODE_SENTENCE_EL.addEventListener("click", e => set_mode(MODE_SENTENCE)); 64 65let sentence_loader = new SentenceLoader( 66 document.getElementById("sentence-config") 67); 68 69function did_win() { 70 return document.getElementById("under-text").classList.contains("won"); 71} 72 73let current_text = null; // Object generated from sentence_loader 74 75sentence_loader.load_from_localstorage().then(() => { 76 current_text = sentence_loader.next_text(); 77 morse.force_update = true; 78}); 79 80function win() { 81 if (did_win()) 82 return; 83 84 document.getElementById("accuracy").innerText = ""; 85 86 document.getElementById("under-text").classList.add("won"); 87 88 let s = stats(); 89 90 let acc = Math.round(s.accuracy_overall * 10000) / 100; 91 document.getElementById("accuracy-result").innerText = acc + "%"; 92 document.getElementById("wpm-result").innerText = morse.get_speed_char() + "/" + morse.get_speed_word(); 93 document.getElementById("chars-result").innerText = current_text.text.length; 94 document.getElementById("mode-result").innerText = sentence_loader.current_generator.describe(); 95 96 history.add_commit_and_rerender({ 97 "complete": s.done, 98 "accuracy": s.accuracy_overall, 99 "missed": s.missed, 100 "incorrect": s.incorrect, 101 "extra": s.extra, 102 "name_short": sentence_loader.current_generator.describe_short(), 103 "name_long": sentence_loader.current_generator.describe(), 104 }); 105 106 audio.stop(); 107 108 if (s.accuracy_overall > 0.90) { 109 sentence_loader.completed(current_text); 110 111 let fw = make_fireworks(); 112 } 113} 114 115function focus_key() { 116 document.getElementById("text-inp").focus(); 117} 118 119function unwin() { 120 document.getElementById("under-text").classList.remove("won"); 121} 122 123let waiting_confirm = false; 124 125let reset_warning = document.getElementById("reset-confirm"); 126 127function new_sentence() { 128 if (!waiting_confirm && !did_win()) { 129 reset_warning.classList.add("active"); 130 waiting_confirm = true; 131 } else { 132 reset_warning.classList.remove("active"); 133 waiting_confirm = false;
134 if (did_win()) { 135 current_text = sentence_loader.next_text(); 136 last_played_idx = 0; 137 morse.typed_text = ""; 138 morse.force_update = true; 139 audio.stop(); 140 unwin(); 141 focus_key(); 142 } else { 143 win(); 144 } 145 } 146} 147 148function unconfirm_reset() { 149 reset_warning.classList.remove("active"); 150 waiting_confirm = false; 151} 152 153let button_play = document.getElementById("button-play-outer"); 154let button_stop = document.getElementById("button-stop-outer"); 155let button_next = document.getElementById("button-next-outer"); 156 157button_play.addEventListener("click", e => { 158 unconfirm_reset(); 159 audio.init_user(); 160 play_current(); 161 focus_key(); 162}); 163 164button_stop.addEventListener("click", e => { 165 unconfirm_reset(); 166 audio.init_user(); 167 audio.stop() 168}); 169 170button_next.addEventListener("click", e => { 171 new_sentence(); 172}); 173 174document.body.addEventListener("keydown", e => { 175 audio.init_user(); 176 if (e.key == "Tab") { 177 new_sentence(); 178 e.preventDefault(); 179 } 180 if (e.key == "Escape") { 181 unconfirm_reset(); 182 audio.stop(); 183 } 184 if (e.key == " " && did_win()) { 185 new_sentence(); 186 e.preventDefault(); 187 } 188}); 189 190document.getElementById("key").addEventListener("click", e => { 191 unconfirm_reset(); 192 audio.init_user(); 193 document.getElementById("text-inp").focus(); 194}); 195 196document.getElementById("text-inp").addEventListener("input", e => { 197 unconfirm_reset(); 198 audio.init_user(); 199 e.target.value = e.target.value.toUpperCase(); 200}); 201 202document.getElementById("text-inp").addEventListener("keydown", e => { 203 audio.init_user(); 204 if (e.key == " " || e.key == "Enter") { 205 e.preventDefault(); 206 e.stopPropagation(); 207 208 if (current_mode === MODE_SENTENCE && !audio.is_playing && last_played_idx < current_text.text.length && !did_win() && e.target.value == "") { 209 play_current(); 210 return; 211 } 212 213 let typed = e.target.value; 214 e.target.value = ""; 215 216 if (did_win()) { 217 waiting_confirm = true; 218 new_sentence(); 219 return; 220 } 221 222 unconfirm_reset(); 223 224 morse.typed_text += typed + " "; 225 cut_text(); 226 morse.force_update = true; 227 228 if (current_mode === MODE_WORD) { 229 play_current(); 230 } 231 } 232}); 233 234function on_audio_off() { 235 console.log("done!"); 236 button_play.classList.add("active"); 237 button_stop.classList.remove("active"); 238 239 morse.force_update = true; 240} 241 242on_audio_off(); 243 244function play_current() { 245 button_play.classList.remove("active"); 246 button_stop.classList.add("active"); 247 248 audio.off(); 249 250 if (current_mode === MODE_SENTENCE) { 251 let remaining = current_text.text.slice(last_played_idx); 252 audio.play(remaining); 253 } else if (current_mode === MODE_WORD) { 254 let pause = audio.next_at != null; 255 256 let w = ""; 257 258 for (let ch of current_text.text.slice(morse.typed_text.length)) { 259 if (ch == " ") 260 break; 261 w += ch; 262 } 263 264 audio.play(w); 265 if (pause) 266 audio.next_at = Date.now() + morse.WORD_SEP * 1000; 267 } 268} 269 270function redact(text) { 271 let redacted = ""; 272 for (let ch of text) { 273 redacted += ch == " " ? " " : "?"; 274 } 275 return redacted; 276} 277 278function cut_text() { 279 if (current_text.text === null) { 280 return 281 } 282 if (current_mode === MODE_WORD) { 283 morse.typed_text += " "; 284 for (let i = 0; i < morse.typed_text.length; i++) { 285 if (morse.typed_text[i] != current_text.text[i]) { 286 morse.typed_text = morse.typed_text.slice(0, i); 287 typed = morse.typed_text; 288 break; 289 } 290 if (i == current_text.text.length - 1) { 291 win(); 292 return; 293 } 294 } 295 } else { 296 let units = distance(current_text.text, morse.typed_text.trim()); 297 if (units.length === 0) { 298 console.error("No distance units?"); 299 } 300 let last = units[units.length - 1]; 301 if (last.type !== "missing_end") { 302 win(); 303 } 304 } 305} 306 307function realize(ch) { 308 if (ch === " ") { 309 return "â£"; 310 } 311 return ch; 312} 313 314// { 315// "done": bool, 316// "accuracy_current": float, 317// "accuracy_overall": float, 318// "missed": int, 319// "incorrect": int, 320// "extra": int, 321// } 322function stats() { 323 let units = distance(current_text.text, morse.typed_text.trim()); 324 325 let n_correct_nonspace = 0; 326 let n_nonspace = 0; 327 let accuracy_current = null; 328 329 let n_missed = 0; 330 let n_incorrect = 0; 331 let n_extra = 0; 332 333 let done = true; 334 335 for (let unit of units) { 336 if (unit.type === "correct") { 337 n_correct_nonspace++; 338 } 339 if (unit.type === "incorrect") { 340 n_incorrect++; 341 } 342 if (unit.type === "extraneous") { 343 n_extra++; 344 } 345 if (unit.type === "missing") { 346 n_missed++; 347 } 348 if (unit.type === "missing_end") { 349 accuracy_current = n_correct_nonspace / n_nonspace; 350 done = false;
351 n_nonspace = current_text.text.length; 352 break; 353 } 354 n_nonspace++; 355 } 356 357 accuracy_overall = n_correct_nonspace / n_nonspace; 358 359 if (accuracy_current === null) { 360 accuracy_current = accuracy_overall; 361 } 362 363 return { 364 "done": done, 365 "accuracy_current": accuracy_current, 366 "accuracy_overall": accuracy_overall, 367 "missed": n_missed, 368 "incorrect": n_incorrect, 369 "extra": n_extra, 370 }; 371} 372 373function update_display(typed, typing, morse_spans, text) { 374 // TODO: Handle line breaking? Can we group the letter-spans into word-spans/divs maybe? 375 376 if (current_text === null) { 377 // Does this ever happen? 378 379 document.getElementById("author").innerText = ""; 380 document.getElementById("text").replaceChildren(); 381 document.getElementById("accuracy").innerText = ""; 382 return 383 } 384 385 if (current_text.author != null) { 386 document.getElementById("author").innerText = "- " + current_text.author; 387 } else { 388 document.getElementById("author").innerText = ""; 389 } 390 391 if (typed.length > 0) { 392 let s = stats(); 393 let acc = Math.round(s.accuracy_current * 100); 394 document.getElementById("accuracy").innerText = "(" + acc + "% accuracy so far)"; 395 } else { 396 document.getElementById("accuracy").innerText = ""; 397 } 398 399 let text_div = document.getElementById("text"); 400 401 let units = distance(current_text.text, did_win() ? typed.trim() : typed); 402 let elements = []; 403 for (let unit of units) { 404 let el = document.createElement("span"); 405 el.classList.add("char"); 406 if (unit.type === "correct") { 407 el.innerText = unit.char; 408 el.classList.add("correct"); 409 } else if (unit.type === "missing") { 410 el.innerText = realize(unit.char); 411 el.classList.add("missing"); 412 } else if (unit.type === "extraneous") { 413 el.innerText = realize(unit.char); 414 el.classList.add("extraneous"); 415 } else if (unit.type === "incorrect") { 416 el.innerText = realize(unit.typed); 417 el.classList.add("incorrect"); 418 let corrected = document.createElement("span"); 419 corrected.classList.add("corrected"); 420 corrected.innerText = realize(unit.given); 421 el.appendChild(corrected); 422 } else if (unit.type === "missing_end") { 423 let rest = current_text.text.slice(-unit.length); 424 el.innerText = did_win() ? rest : redact(rest); 425 el.classList.add("missing-end"); 426 } else if (unit.type === "extra_rest") { 427 let rest = typed.slice(-unit.length); 428 el.innerText = rest; 429 el.classList.add("extra-rest"); // TODO: When would this happen? This should probably be the win condition 430 } 431 elements.push(el); 432 } 433 text_div.replaceChildren(...elements); 434}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.