PageSourceSearch

https://coral.shoes/morse-trainer/rx.js

js coral.shoes collected 2026-10-09 23:57:04 UTC 12,294 bytes, 434 lines download raw bytes

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.