1const DIT = "."; 2const DAH = "-"; 3const LETTER_SEPARATOR = "LET"; 4const WORD_SEPARATOR = "WORD"; 5 6const DITS_PER_WORD = 50; // paris 7 8const TUNE = [ 9 0, 0, 0, 12, 7, 6, 5, 3, 0, 3, 5, 10 -2, -2, 12, 7, 6, 5, 3, 0, 3, 5, 11 -4, -4, 12, 7, 6, 5, 3, 0, 3, 5, 12 -5, -5, 12, 7, 6, 5, 3, 0, 3, 5, 13 3, 3, 3, 3, 3, 0, 0, 14 3, 3, 3, 3, 5, 6, 5, 3, 0, 3, 5, 15 3, 3, 3, 5, 7, 10, 10, 7, 16 12, 12, 12, 7, 12, 10, 12+5 17]; 18 19function event_span(ev) { 20 let el = document.createElement("span"); 21 if (ev == DIT) { 22 el.innerText = "·"; 23 el.classList.add("dot"); 24 } else if (ev == DAH) { 25 el.innerText = "â"; 26 el.classList.add("dot"); 27 } else if (ev == LETTER_SEPARATOR) { 28 el.innerText = "-"; 29 el.classList.add("space"); 30 } else if (ev == WORD_SEPARATOR) { 31 el.innerText = "â£"; 32 el.classList.add("space"); 33 } 34 return el; 35} 36 37function span_with_class(text, ...clss) { 38 let el = document.createElement("span"); 39 40 el.innerText = text; 41 42 for (let cls of clss) 43 el.classList.add(cls); 44 return el; 45} 46 47function get_letter(ev) { 48 for (var i = 0; i < MORSE.length; i++) { 49 let l = MORSE[i]; 50 if (l.length == 0) 51 continue; 52 let sequence = l[1]; 53 if (sequence.length != ev.length) 54 continue; 55 56 let bad = false; 57 for (var j = 0; j < sequence.length; j++) { 58 if ((sequence[j] == ".") != (ev[j] == DIT)) 59 bad = true; 60 } 61 if (!bad) 62 return l[0]; 63 } 64 return "?"; 65} 66 67function get_word(events, use_real_spaces) { 68 let letters = []; 69 let current_letter = []; 70 for (var i = 0; i < events.length; i++) { 71 let current_ev = events[i]; 72 if (current_ev == DIT || current_ev == DAH) { 73 current_letter.push(current_ev); 74 } else if (current_ev == LETTER_SEPARATOR || current_ev == WORD_SEPARATOR) { 75 letters.push(get_letter(current_letter)); 76 if (current_ev == WORD_SEPARATOR) 77 letters.push(use_real_spaces ? " " : "â£"); 78 current_letter = []; 79 } 80 } 81 return letters.join(""); 82} 83 84// empty element = space in table 85const MORSE = [ 86 ["A", ".-"], 87 ["B", "-..."], 88 ["C", "-.-."], 89 ["D", "-.."], 90 ["E", "."], 91 ["F", "..-."], 92 ["G", "--."], 93 ["H", "...."], 94 ["I", ".."], 95 ["J", ".---"], 96 ["K", "-.-"], 97 ["L", ".-.."], 98 ["M", "--"], 99 ["N", "-."], 100 ["O", "---"], 101 ["P", ".--."], 102 ["Q", "--.-"], 103 ["R", ".-."], 104 ["S", "..."], 105 ["T", "-"], 106 ["U", "..-"], 107 ["V", "...-"], 108 ["W", ".--"], 109 ["X", "-..-"], 110 ["Y", "-.--"], 111 ["Z", "--.."], 112 113 [], 114 115 ["1", ".----"], 116 ["2", "..---"], 117 ["3", "...--"], 118 ["4", "....-"], 119 ["5", "....."], 120 ["6", "-...."], 121 ["7", "--..."], 122 ["8", "---.."], 123 ["9", "----."], 124 ["0", "-----"], 125 126 [], 127 128 [".", ".-.-.-"], 129 [",", "--..--"], 130 ["?", "..--.."], 131 ["!", "-.-.--"], 132]; 133 134class Morse { 135 // draw_callback(typed, typing, morse_spans, current_word) 136 constructor(default_wpm, draw_callback) { 137 this.draw_callback = draw_callback; 138 139 this.last_press_start = null; 140 this.last_press_end = null; 141 this.events = []; 142 143 this.typed_text = ""; 144 145 this.force_update = false; 146 147 this.DIT_DURATION = 0; 148 this.DAH_DURATION = 0; 149 this.SYMBOL_SEP = 0; 150 this.LETTER_SEP = 0; 151 this.WORD_SEP = 0; 152 153 this.set_speed_char(default_wpm); 154 this.set_speed_word(default_wpm); 155 156 let self = this; 157 this.redraw_int = setInterval(() => self.redraw(), 10); 158 159 this.last_displayed_events = null; 160 } 161 162 // All sets and gets are in wpm unless specified as other 163 set_speed_char(wpm) { 164 let dit_speed_s = 1 / (wpm / 60 * DITS_PER_WORD); 165 166 this.DIT_DURATION = dit_speed_s; 167 this.DAH_DURATION = 3 * dit_speed_s; 168 this.SYMBOL_SEP = dit_speed_s; 169 } 170 171 set_speed_word(wpm) { 172 let dit_speed_s = 1 / (wpm / 60 * DITS_PER_WORD); 173 174 this.LETTER_SEP = 3 * dit_speed_s; 175 this.WORD_SEP = 7 * dit_speed_s; 176 } 177 178 get_speed_char() { 179 let wpm = (1 / this.DIT_DURATION) / DITS_PER_WORD * 60; 180 return Math.round(wpm * 1000) / 1000; 181 } 182 183 get_speed_word() { 184 let dit_word = this.WORD_SEP / 7; 185 let wpm = (1 / dit_word) / DITS_PER_WORD * 60; 186 // round because floating point inprecision 187 return Math.round(wpm * 1000) / 1000; 188 } 189 190 // set_speeds_ms(dit_speed_ms) { 191 // this.DIT_DURATION = dit_speed_ms / 1000; 192 // this.DAH_DURATION = 3 * this.DIT_DURATION; 193 // this.SYMBOL_SEP = this.DIT_DURATION; 194 // this.LETTER_SEP = 3 * this.DIT_DURATION; 195 // this.WORD_SEP = 7 * this.DIT_DURATION; 196 // } 197 198 // set_speeds_wpm(wpm) { 199 // let s_per_dit = 1 / (wpm / 60 * DITS_PER_WORD); 200 // this.set_speeds_ms(1000 * s_per_dit); 201 // } 202 203 // get_dit_speed_ms() { 204 // return 0 | (this.DIT_DURATION * 1000); 205 // } 206 207 // get_dit_speed_wpm() { 208 // let wpm = (1 / this.DIT_DURATION) / DITS_PER_WORD * 60; 209 210 // return (0 | (10 * wpm)) / 10; 211 // } 212 213 get_space() { 214 if (this.last_press_end == null) { 215 return null; 216 } 217 let delta = (Date.now() - this.last_press_end) / 1000; 218 if (delta > (this.WORD_SEP + this.LETTER_SEP) / 2) { 219 return WORD_SEPARATOR; 220 } else if (delta > (this.LETTER_SEP + this.SYMBOL_SEP) / 2) { 221 return LETTER_SEPARATOR; 222 } 223 return null; 224 } 225 226 get_dot() { 227 if (this.last_press_start == null) { 228 return null; 229 } 230 let delta = (Date.now() - this.last_press_start) / 1000; 231 if (delta > (this.DAH_DURATION + this.DIT_DURATION) / 2) { 232 return DAH; 233 } else { 234 return DIT; 235 } 236 } 237 238 push_word() { 239 let word = get_word(this.events, true); 240 this.typed_text += word; 241 this.events = []; 242 } 243 244 press() { 245 this.last_press_start = Date.now(); 246 247 let space = this.get_space(); 248 if (space != null) 249 this.events.push(space); 250 251 this.last_press_end = null; 252 253 if (space == WORD_SEPARATOR) { 254 this.push_word(); 255 } 256 257 this.redraw(); 258 } 259 260 submit_word() { 261 let space = this.get_space(); 262 if (space != null) 263 this.events.push(space); 264 this.push_word(); 265 266 this.events = []; 267 this.last_press_start = null; 268 this.last_press_end = null; 269 } 270 271 release() { 272 this.last_press_end = Date.now(); 273 274 let dot = this.get_dot(); 275 if (dot != null) 276 this.events.push(dot); 277 278 this.last_press_start = null; 279 280 if (this.events.length == 0) 281 this.last_press_end = null; 282 283 this.redraw(); 284 } 285 286 clear() { 287 this.events = []; 288 this.last_press_start = null; 289 this.last_press_end = null; 290 this.force_update = true; 291 } 292 293 clear_all() { 294 this.typed_text = ""; 295 this.clear(); 296 } 297 298 redraw() { 299 let current_events = this.events.slice(); 300 301 let dot = this.get_dot(); 302 if (dot != null) 303 current_events.push(dot); 304 305 let space = this.get_space(); 306 if (space != null) 307 current_events.push(space); 308 309 if (!this.force_update && this.last_displayed_events != null && current_events.join(" ") == this.last_displayed_events.join(" ")) 310 return; 311 this.force_update = false;
312 this.last_displayed_events = current_events; 313 314 315 let content = []; 316 for (var i = 0; i < current_events.length; i++) { 317 let thing = current_events[i]; 318 content.push(event_span(thing)); 319 } 320 let current_word = get_word(current_events, false); 321 322 this.draw_callback(this.typed_text, get_word(current_events, false), content, current_word); 323 } 324} 325 326function bind_speed_input(morse, split_cb, wpm_inp, cwpm_inp, wwpm_inp) { 327 if (localStorage.getItem("speed-char") != null && localStorage.getItem("speed-word")) { 328 morse.set_speed_char(+localStorage.getItem("speed-char")); 329 morse.set_speed_word(+localStorage.getItem("speed-word")); 330 } else { 331 morse.set_speed_char(+cwpm_inp.value); 332 morse.set_speed_word(+wwpm_inp.value); 333 } 334 335 if (morse.get_speed_char() === morse.get_speed_word()) { 336 wpm_inp.value = cwpm_inp.value = wwpm_inp.value = morse.get_speed_char(); 337 split_cb.checked = false; 338 } else { 339 split_cb.checked = true; 340 cwpm_inp.value = morse.get_speed_char(); 341 wwpm_inp.value = morse.get_speed_word(); 342 wpm_inp.value = morse.get_speed_word(); // idk 343 } 344 345 function set_same() { 346 morse.set_speed_char(+wpm_inp.value); 347 morse.set_speed_word(+wpm_inp.value); 348 349 localStorage.setItem("speed-char", +wpm_inp.value); 350 localStorage.setItem("speed-word", +wpm_inp.value); 351 } 352 353 function set_split() { 354 morse.set_speed_char(+cwpm_inp.value); 355 morse.set_speed_word(+wwpm_inp.value); 356 357 localStorage.setItem("speed-char", +cwpm_inp.value); 358 localStorage.setItem("speed-word", +wwpm_inp.value); 359 } 360 361 wpm_inp.addEventListener("change", e => set_same()); 362 cwpm_inp.addEventListener("change", e => set_split()); 363 wwpm_inp.addEventListener("change", e => set_split()); 364 split_cb.addEventListener("change", e => { 365 if (split_cb.checked) { 366 set_split(); 367 } else { 368 set_same(); 369 } 370 }); 371 372} 373 374function fill_morse_table(table, f) { 375 let col_left = table.children[0]; 376 let children = []; 377 for (var i = 0; i < MORSE.length / 2; i++) { 378 if (MORSE[i].length == 0) { 379 children.push(document.createElement("br")); 380 continue; 381 } 382 let div = document.createElement("div"); 383 384 div.classList.add("morse-element"); 385 for (var j = 0; j < MORSE[i][1].length; j++) 386 div.appendChild(event_span(MORSE[i][1][j])); 387 388 div.appendChild(span_with_class(" " + MORSE[i][0])); 389 390 children.push(div); 391 392 if (f != undefined) 393 f(div, MORSE[i]); 394 } 395 col_left.replaceChildren(...children); 396 397 let col_right = table.children[2]; 398 children = []; 399 for (; i < MORSE.length; i++) { 400 if (MORSE[i].length == 0) { 401 children.push(document.createElement("br")); 402 continue; 403 } 404 let div = document.createElement("div"); 405 div.classList.add("morse-element"); 406 407 div.appendChild(span_with_class(MORSE[i][0] + " ")); 408 for (var j = 0; j < MORSE[i][1].length; j++) 409 div.appendChild(event_span(MORSE[i][1][j])); 410 411 412 children.push(div); 413 414 if (f != undefined) 415 f(div, MORSE[i]); 416 } 417 col_right.replaceChildren(...children); 418} 419 420 421const AudioContext = window.AudioContext || window.webkitAudioContext; 422 423var HOLD_TIME = 0.03; 424var RAMP_TIME = 0.012; 425 426class MorseAudio { 427 // lamp = document element to be given and ungiven .light class 428 // may be undefined 429 430 // as some browsers (firefox...) requires AudioContexts to be created 431 // as a result of a "user action", we initially set audio_ctx to undefined. 432 // Whenever a user action occurs, the controlling code should call the 433 // init_user() method, which sets up the audio_ctx 434 435 constructor(morse, lamp) { 436 this.morse = morse; 437 this.lamp = lamp; 438 lamp.children[0].focus(); 439 440 this.audio_ctx = undefined; 441 442 this._freq = 512; 443 this.needs_freq_update = false; 444 this._volume = 1; 445 this.needs_volume_update = false;
446 447 this.is_on = false; 448 449 // Lists of functions 450 this.on_play_ends = []; 451 this.on_play_letters = []; 452 453 this.playing = []; // [["on"|"off", "dit"|"dah"|"symb"|"let"|"word", idx]] 454 this.is_playing = false; 455 this.next_at = Date.now(); 456 457 let self = this; 458 setInterval(() => self.tick(), 10); 459 460 this.tune_enabled = false; 461 this.tune_index = 0; 462 } 463 464 init_user() { 465 if (this.audio_ctx !== undefined) 466 return; 467 468 this.audio_ctx = new AudioContext(); 469 470 this.base_osc = this.audio_ctx.createOscillator(); 471 this.base_osc.frequency.setValueAtTime(512, this.audio_ctx.currentTime); 472 473 this.gain = this.audio_ctx.createGain(); 474 this.gain.gain.setValueAtTime(0, this.audio_ctx.currentTime); 475 476 this.base_osc.connect(this.gain); 477 this.gain.connect(this.audio_ctx.destination); 478 this.base_osc.start(); 479 480 console.info("Audio initialized"); 481 482 if (this.is_on) { 483 this.is_on = false; 484 this.on(); 485 } 486 } 487 488 set_freq(freq_hz) { 489 if (freq_hz === this._freq) 490 return; 491 this._freq = freq_hz; 492 this.needs_freq_update = true; 493 } 494 495 set_volume(volume) { 496 this._volume = volume / 250; 497 if (this.is_on) 498 this.on(); 499 } 500 501 set_light_enabled(status) { 502 if (status) { 503 this.lamp_enabled = true; 504 } else { 505 this.lamp_enabled = false; 506 if (this.lamp !== undefined) 507 this.lamp.classList.remove("light"); 508 } 509 } 510 511 _freq_multiplier() { 512 if (this.tune_enabled) { 513 return Math.pow(2, TUNE[this.tune_index] / 12); 514 } else { 515 return 1; 516 } 517 } 518 519 tick() { 520 if (this.needs_freq_update && this.audio_ctx !== undefined) { 521 this.base_osc.frequency.setValueAtTime(this._freq * this._freq_multiplier(), this.audio_ctx.currentTime); 522 this.needs_freq_update = false; 523 } 524 if (this.next_at == null || Date.now() > this.next_at) { 525 if (this.playing.length > 0) { 526 let next = this.playing[0]; 527 this.playing = this.playing.slice(1); 528 529 if (next[0] == "on") 530 this.on(); 531 else 532 this.off(); 533 534 let duration = null; 535 if (next[1] == "dit") 536 duration = this.morse.DIT_DURATION; 537 else if (next[1] == "dah") 538 duration = this.morse.DAH_DURATION; 539 else if (next[1] == "symb") 540 duration = this.morse.SYMBOL_SEP; 541 else if (next[1] == "let") 542 duration = this.morse.LETTER_SEP; 543 else if (next[1] == "word") 544 duration = this.morse.WORD_SEP; 545 546 if (duration == null) { 547 console.log(`Unknown duration for ${next}`); 548 return; 549 } 550 this.next_at = Date.now() + duration * 1000; 551 for (let on_play_letter of this.on_play_letters) 552 on_play_letter(next[2]); 553 } else { 554 if (this.next_at != null) { 555 this.next_at = null; 556 this.off(); 557 558 this.is_playing = false; 559 for (let on_play_end of this.on_play_ends) 560 on_play_end(); 561 } 562 } 563 } 564 } 565 566 play(text) { 567 this.playing = []; 568 let last_letter = false; 569 for (var i = 0; i < text.length; i++) { 570 let char_here = text[i]; 571 572 if (char_here == " " || char_here == "\n") { 573 if (last_letter) 574 this.playing = this.playing.slice(0, this.playing.length-1); // remove last symbol separator 575 this.playing.push(["off", "word", i]); 576 last_letter = false; 577 } else { 578 let code = null; 579 for (let m of MORSE) { 580 if (m.length == 0) 581 continue; 582 if (m[0] == char_here) 583 code = m[1]; 584 } 585 if (code == null) { 586 console.warn(`unknown char ${char_here}`); 587 continue; 588 } 589 for (let ch of code) { 590 if (ch == DIT) { 591 this.playing.push(["on", "dit", i]); 592 this.playing.push(["off", "symb", i]); 593 } else { 594 this.playing.push(["on", "dah", i]); 595 this.playing.push(["off", "symb", i]); 596 } 597 } 598 // Remove last letter symbol separator 599 this.playing = this.playing.slice(0, this.playing.length-1); 600 this.playing.push(["off", "let", i]); 601 last_letter = true; 602 } 603 this.is_playing = true; 604 } 605 } 606 607 stop() { 608 this.next_at = null; 609 this.playing = []; 610 this.off(); 611 612 this.is_playing = false;
613 for (let on_play_end of this.on_play_ends) 614 on_play_end(); 615 } 616 617 on() { 618 if (this.is_on) 619 return; 620 if (this.audio_ctx === undefined) 621 return; 622 this.gain.gain.setValueAtTime(0, this.audio_ctx.currentTime+HOLD_TIME); 623 this.gain.gain.linearRampToValueAtTime(this._volume, this.audio_ctx.currentTime+HOLD_TIME+RAMP_TIME); 624 if (this.lamp !== undefined && this.lamp_enabled) 625 this.lamp.classList.add("light"); 626 627 this.is_on = true; 628 629 if (this.tune_enabled) { 630 this.tune_index += 1; 631 this.tune_index %= TUNE.length; 632 this.needs_freq_update = true; 633 } 634 } 635 636 off() { 637 if (!this.is_on) 638 return; 639 if (this.audio_ctx === undefined) 640 return; 641 this.gain.gain.setValueAtTime(this._volume, this.audio_ctx.currentTime+HOLD_TIME); 642 this.gain.gain.linearRampToValueAtTime(0, this.audio_ctx.currentTime+HOLD_TIME+RAMP_TIME); 643 if (this.lamp !== undefined) 644 this.lamp.classList.remove("light"); 645 646 this.is_on = false; 647 } 648} 649 650function bind_volume_input(audio, volume_inp) { 651 if (localStorage.getItem("volume") != null) { 652 audio.set_volume(+localStorage.getItem("volume")); 653 volume_inp.value = localStorage.getItem("volume"); 654 } else { 655 audio.set_volume(+volume_inp.value); 656 } 657 658 volume_inp.addEventListener("change", e => { 659 audio.set_volume(+volume_inp.value); 660 localStorage.setItem("volume", volume_inp.value); 661 }); 662} 663 664function bind_enable_light(audio, enable_light_checkbox) { 665 if (localStorage.getItem("enable_light") != null) { 666 audio.set_light_enabled(+localStorage.getItem("enable_light")); 667 enable_light_checkbox.checked = localStorage.getItem("enable_light"); 668 } else { 669 audio.set_light_enabled(0); 670 } 671 672 enable_light_checkbox.addEventListener("change", e => { 673 audio.set_light_enabled(+enable_light_checkbox.checked); 674 localStorage.setItem("enable_light", +enable_light_checkbox.checked); 675 }); 676} 677 678function bind_frequency_input(audio, freq_inp) { 679 if (localStorage.getItem("freq") != null) { 680 audio.set_freq(+localStorage.getItem("freq")); 681 freq_inp.value = localStorage.getItem("freq"); 682 } else { 683 audio.set_freq(+freq_inp.value); 684 } 685 686 freq_inp.addEventListener("change", e => { 687 audio.set_freq(+freq_inp.value); 688 localStorage.setItem("freq", freq_inp.value); 689 }); 690} 691 692class SelectionHandler { 693 // play_start(text, start, end) 694 // on_play_end() 695 // on_play_letter(idx) 696 constructor(audio, text_field, play_button, stop_button, on_play_start, on_play_end, on_play_letter) { 697 this.audio = audio; 698 699 this.text_field = text_field; 700 this.play_button = play_button; 701 this.stop_button = stop_button; 702 703 704 this.on_play_start = on_play_start; 705 this.on_play_end = on_play_end; 706 this.on_play_letter = on_play_letter; 707 708 this.selection_offset = null; 709 710 let self = this; 711 712 audio.on_play_ends.push(() => self.on_play_end()); 713 audio.on_play_letters.push((idx) => { 714 self.on_play_letter(idx + self.selection_offset) 715 }); 716 717 this.play_button.addEventListener("click", () => self.play()); 718 this.stop_button.addEventListener("click", () => self.stop()); 719 setInterval(() => self.check(), 10); 720 721 this.last_selection = this.get_selection(); 722 } 723 724 get_selection() { 725 let sel = window.getSelection(); 726 727 let selected = true; 728 if ((sel.focusNode == null) || (sel.anchorNode == null)) { 729 selected = false; 730 } else if (!sel.focusNode.parentElement.isEqualNode(this.text_field)) { 731 selected = false; 732 } else if (!sel.anchorNode.parentElement.isEqualNode(this.text_field)) 733 selected = false; 734 else if (sel.toString().length == 0) { 735 selected = false; 736 } 737 738 if (!selected) 739 return null; 740 741 let start = Math.min(sel.anchorOffset, sel.focusOffset); 742 let end = Math.max(sel.anchorOffset, sel.focusOffset); 743 744 return {"start": start, "end": end, "text": sel.toString()}; 745 } 746 747 check() { 748 let selection = this.get_selection(); 749 if (selection == null) { 750 this.play_button.classList.remove("active"); 751 if (!this.play_button.contains(window.getSelection().focusNode)) { 752 this.last_selection = null; 753 } 754 } else { 755 this.play_button.classList.add("active"); 756 this.last_selection = selection; 757 } 758 } 759 760 play() { 761 if (this.last_selection == null) 762 return; 763 764 this.selection_offset = this.last_selection["start"]; 765 766 this.audio.play(this.last_selection["text"]); 767 768 this.on_play_start(this.last_selection["text"], this.last_selection["start"], this.last_selection["end"]); 769 770 this.last_selection = null; 771 this.stop_button.classList.add("active"); 772 } 773 774 stop() { 775 this.audio.stop(); 776 this.stop_button.classList.remove("active"); 777 } 778} 779 780function hue2rgb(hue) { 781 let theta = 2 * Math.PI * hue;
782 let r = Math.cos(theta); 783 let g = Math.cos(theta + 2 * Math.PI / 3); 784 let b = Math.cos(theta + 2 * 2 * Math.PI / 3); 785 return [(r + 1) / 2, (g + 1) / 2, (b + 1) / 2]; 786} 787 788class Fireworks { 789 constructor(canvas_element) { 790 this.canvas_element = canvas_element; 791 this.ctx = this.canvas_element.getContext("2d"); 792 793 let self = this; 794 795 this.particles = []; 796 for (let i = 0; i < 5; i++) { 797 let x = Math.random() * this.canvas_element.offsetWidth; 798 let y = Math.random() * this.canvas_element.offsetHeight; 799 800 this.particles.push([x, y, (Math.random() - 0.5) * 50, -50, Math.random(), 2, 0.2 + 0.5 * Math.random()]); 801 } 802 this.gravity = 100; 803 this.dt = 0.01; 804 this.render_internal = setInterval(() => self.render(), this.dt * 1000); 805 } 806 807 render() { 808 this.canvas_element.width = this.canvas_element.offsetWidth; 809 this.canvas_element.height = this.canvas_element.offsetHeight; 810 811 this.ctx.clearRect(0, 0, this.canvas_element.width, this.canvas_element.height); 812 813 let new_particles = []; 814 for (let particle of this.particles) { 815 let x = particle[0]; 816 let y = particle[1]; 817 let vx = particle[2]; 818 let vy = particle[3]; 819 let hue = particle[4]; 820 let count = particle[5]; 821 let timeLeft = particle[6]; 822 823 x += vx * this.dt; 824 y += vy * this.dt; 825 vy += this.gravity * this.dt; 826 827 timeLeft -= this.dt; 828 829 this.ctx.lineWidth = 2; 830 831 let [r, g, b] = hue2rgb(particle[4]); 832 this.ctx.beginPath(); 833 this.ctx.strokeStyle = `rgb(${256*r}, ${256*g}, ${256*b})`; 834 this.ctx.lineWidth = 1.5 * count + 2; 835 this.ctx.moveTo(x, y); 836 this.ctx.lineTo(x + vx * 0.1 * (count + 1), y + vy * 0.1 * (count + 1)); 837 this.ctx.stroke(); 838 839 particle[0] = x; 840 particle[1] = y; 841 particle[2] = vx; 842 particle[3] = vy; 843 particle[4] = hue; 844 particle[5] = count; 845 particle[6] = timeLeft; 846 if (timeLeft > 0) { 847 if (x > 0 && x < this.canvas_element.width && y > 0 && y < this.canvas_element.height) 848 new_particles.push(particle); 849 } else if (count > 0) { 850 for (let i = 0; i < 20; i++) { 851 let u = Math.random(); 852 let v = Math.random(); 853 854 let z1 = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); 855 let z2 = Math.sqrt(-2 * Math.log(u)) * Math.sin(2 * Math.PI * v); 856 857 let child_hue = hue + Math.random() / 5; 858 859 this.particles.push([x, y, z1 * 40, z2 * 40, child_hue, count - 1, Math.random()]); 860 } 861 } 862 } 863 this.particles = new_particles; 864 865 if (this.particles.length == 0) { 866 this.destroy(); 867 } 868 } 869 870 destroy() { 871 this.canvas_element.remove(); 872 clearInterval(this.render_internal); 873 } 874} 875 876function make_fireworks() { 877 let fireworks_element = document.createElement("canvas"); 878 fireworks_element.classList.add("fireworks"); 879 880 document.getElementById("main-split").append(fireworks_element); 881 882 return new Fireworks(fireworks_element); 883} 884 885// shamelessly stolen from https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript#comment111181647_8831937 886function strhash(st) { 887 return Array.from(st).reduce((hash, char) => 0 | (31 * hash + char.charCodeAt(0)), 0); 888} 889 890const PRESETS = [ 891 {"id": "quotes", "name": "Quotes", "type": "quotes"}, 892 {"id": "all", "name": "All chars", "type": "markov", "subset": "KMRSAUPTLOWI.NJEF0YV,G5Q9ZH38B427C1D6X?!"}, 893 // {"id": "hamgen", "name": "Fake ham", "type": "hamgen"}, 894 null, 895 {"id": "k10", "name": "Koch 10%", "type": "markov", "important": "KMRS", "subset": "KMRS"}, 896 {"id": "k20", "name": "Koch 20%", "type": "markov", "important": "AUPT", "subset": "KMRSAUPT"}, 897 {"id": "k30", "name": "Koch 30%", "type": "markov", "important": "LOWI", "subset": "KMRSAUPTLOWI"}, 898 {"id": "k40", "name": "Koch 40%", "type": "markov", "important": ".NJE", "subset": "KMRSAUPTLOWI.NJE"}, 899 {"id": "k50", "name": "Koch 50%", "type": "markov", "important": "F0YV", "subset": "KMRSAUPTLOWI.NJEF0YV"}, 900 {"id": "k60", "name": "Koch 60%", "type": "markov", "important": ",G5Q", "subset": "KMRSAUPTLOWI.NJEF0YV,G5Q"}, 901 {"id": "k70", "name": "Koch 70%", "type": "markov", "important": "9ZH3", "subset": "KMRSAUPTLOWI.NJEF0YV,G5Q9ZH3"}, 902 {"id": "k80", "name": "Koch 80%", "type": "markov", "important": "8B42", "subset": "KMRSAUPTLOWI.NJEF0YV,G5Q9ZH38B42"}, 903 {"id": "k90", "name": "Koch 90%", "type": "markov", "important": "7C1D", "subset": "KMRSAUPTLOWI.NJEF0YV,G5Q9ZH38B427C1D"}, 904 {"id": "k100", "name": "Koch 100%", "type": "markov", "important": "6X?!", "subset": "KMRSAUPTLOWI.NJEF0YV,G5Q9ZH38B427C1D6X?!"}, 905 null, 906 {"id": "vow", "name": "Vowels", "type": "markov", "important": "", "subset": "AOEUIY"}, 907 {"id": "cons", "name": "Consonants", "type": "markov", "important": "", "subset": "PFGCRLDHTNSQJKXBMWVZ"}, 908 {"id": "nums", "name": "Numbers", "type": "markov", "important": "", "subset": "0123456789"}, 909 {"id": "punct", "name": "Punctuation", "type": "markov", "important": "", "subset": ",.!?"}, 910 null, 911 {"id": "a", "name": "A's (·â)", "type": "markov", "important": "", "subset": "AJLPRW"}, 912 {"id": "i", "name": "I's (··)", "type": "markov", "important": "", "subset": "FHISUV"}, 913 {"id": "n", "name": "N's (â·)", "type": "markov", "important": "", "subset": "BCDKNXY"}, 914 {"id": "m", "name": "M's (ââ)", "type": "markov", "important": "", "subset": "MOGQZ"}, 915]; 916 917class TextGenerator { 918 async load_resources() { throw "Abstract method called" } 919 920 // for result screen. returns string 921 describe() { throw "Abstract method called" } 922 923 // for history 924 describe_short() { throw "Abstract method called" } 925 926 // reteurns bool 927 is_loaded() { throw "Abstract method called" } 928 929 // returns {"text": "...", "author": <optional>} 930 next_text() { throw "Abstract method called" } 931 932 // argument text is what next_text returned 933 // may return dict of attributes about the text to show after completion 934 // might not be called after every next_text 935 text_completed(sentence) { throw "Abstract method called" } 936 937 // Called every "action" with a div below the mode selector 938 // Element might contain previous content 939 // Call cb_save when user interacts with the state of the generator such that the parent sentence loader needs to save 940 render_sidebar(sidebar, cb_save) { throw "Abstract method called" } 941 942 // Called when preset is loaded or from localStorage 943 set_data(data) { throw "Abstract method called" } 944 945 // set_data(get_data()) should not change the state meaningfully 946 // should contain {"type": "..."} 947 get_data() { throw "Abstract method called" } 948} 949 950class HamGen { 951 async load_resources() { } 952 describe() { return "HamGen"; } 953 describe_short() { return "HamGen"; } 954 is_loaded() { return true; } 955 next_text() { return {"text": "73 DE SA6NYA"}; } 956 text_completed(sentence) { } 957 render_sidebar(sidebar, _cb_save) { 958 sidebar.innerText = "mjau"; 959 } 960 set_data(data) {} 961 get_data(data) { return {"type": "hamgen"}; } 962} 963 964function normalized(arr) { 965 let total = 0; 966 for (let item of arr) 967 total += item; 968 return arr.map(x => x / total); 969} 970 971// Gives an index into the array 972function weighted_pick(arr, noise) { 973 if (Math.random() < noise) { 974 return 0 | (Math.random() * arr.length); 975 } 976 arr = normalized(arr); 977 let x = Math.random(); 978 for (let i = 0; i < arr.length; i++) { 979 x -= arr[i]; 980 if (x < 0) 981 return i; 982 } 983 console.log("OH NO"); 984 return arr.length - 1; 985} 986 987const MINIMUM_MARKOV_LENGTH = 40; 988class MarkovGenerator extends TextGenerator { 989 constructor() { 990 super(); 991 this.min_word_len = 2; 992 this.max_word_len = 6; 993 this.noise = 0.0; 994 995 this.symbols = []; 996 this.forward = []; 997 this.backward = []; 998 this.initial = []; 999
1000 // Invariant: must always contain space 1001 // Both contain numbers indexing into this.symbols 1002 this.subset = []; 1003 this.important = []; 1004 1005 this.loaded = false; 1006 } 1007 1008 describe() { 1009 let self = this; 1010 let letters = [...Array(this.subset.length).keys()].filter(l => !self._terminates(l)); 1011 return "Koch " + this.symbolize(letters); 1012 } 1013 1014 describe_short() { 1015 let count_percent = (this.subset.length - 1) / 40; // -1 = remove space, 40 = length of full 1016 return "Koch " + (0 | count_percent * 100) + "%"; 1017 } 1018 1019 get_subset(arr) { 1020 return this.subset.map(i => arr[i]); 1021 } 1022 1023 async load_resources() { 1024 let self = this; 1025 1026 await fetch("data/markov.json", { 1027 mode: "no-cors", 1028 headers: { 1029 "Content-Type": "application/json" 1030 }, 1031 }).then(data => 1032 data.json() 1033 ).then(data => { 1034 self.symbols = data["symbols"]; 1035 self.forward = data["forward"]; 1036 self.backward = data["backward"]; 1037 self.initial = data["initial"]; 1038 1039 self.subset = [self.symbols.indexOf(" ")]; 1040 self.is_loaded = true; 1041 }); 1042 } 1043 1044 is_loaded() { return this.is_loaded; } 1045 1046 toggle(letter, is_rightclick) { 1047 if (!this.symbols.includes(letter)) 1048 return; 1049 1050 let idx = this.symbols.indexOf(letter); 1051 if (is_rightclick) { 1052 // ensure letter is active 1053 if (!this.subset.includes(idx)) { 1054 this.subset.push(idx); 1055 } 1056 // toggle important 1057 if (this.important.includes(idx)) { 1058 this.important = this.important.filter(x => x != idx); 1059 } else { 1060 this.important.push(idx); 1061 } 1062 } else { 1063 if (this.subset.includes(idx)) { 1064 this.subset = this.subset.filter(a => a != idx); 1065 this.important = this.important.filter(a => a != idx); 1066 } else { 1067 this.subset.push(idx); 1068 } 1069 } 1070 } 1071 1072 has_letter(l) { 1073 return this.get_subset(this.symbols).includes(l); 1074 } 1075 1076 is_on() { 1077 return this.subset.length > 1; 1078 } 1079 1080 _terminates(i) { 1081 return this.symbols[this.subset[i]] == " "; 1082 } 1083 1084 _generate_letter() { 1085 for (let i = 0; i < 10; i++) { 1086 let init = this.get_subset(this.initial); 1087 let letter = weighted_pick(init, this.noise); 1088 if (this._terminates(letter)) 1089 continue; 1090 return letter; 1091 } 1092 return 0; 1093 } 1094 1095 _generate_word(containing_letter) { 1096 while (true) { 1097 let after = []; 1098 let last_ch = containing_letter; 1099 for (let i = 0; i < 10; i++) { 1100 let weights = this.get_subset(this.get_subset(this.forward)[last_ch]); 1101 let pick = weighted_pick(weights, this.noise); 1102 if (this._terminates(pick)) 1103 break; 1104 1105 after.push(pick); 1106 last_ch = pick; 1107 } 1108 1109 let before = []; 1110 last_ch = containing_letter; 1111 for (let i = 0; i < 10; i++) { 1112 let weights = this.get_subset(this.get_subset(this.backward)[last_ch]); 1113 let pick = weighted_pick(weights, this.noise); 1114 if (this._terminates(pick)) 1115 break; 1116 1117 before.push(pick); 1118 last_ch = pick; 1119 } 1120 let pick = before.reverse().concat([containing_letter]).concat(after); 1121 if (pick.length >= this.min_word_len && pick.length <= this.max_word_len) { 1122 return pick; 1123 } 1124 } 1125 } 1126 1127 _generate_text() { 1128 let self = this; 1129 1130 let text = []; 1131 1132 let letters_left_count = []; 1133 for (let i = 0; i < this.subset.length; i++) { 1134 let count = 0; 1135 1136 if (this.important.includes(this.subset[i])) 1137 count = 2; 1138 1139 letters_left_count.push(count); 1140 } 1141 1142 while (letters_left_count.reduce((acc, x) => acc || (x > 0), 0)) { 1143 let letter = weighted_pick(letters_left_count); 1144 let w = this._generate_word(letter); 1145 1146 for (let letter of w) 1147 letters_left_count[letter] = Math.max(letters_left_count[letter] - 1, 0); 1148 1149 text.push(...w); 1150 text.push(this.get_subset(this.symbols).indexOf(" ")); 1151 } 1152 1153 while (text.length < MINIMUM_MARKOV_LENGTH + 1) { 1154 let idx = 0 | (Math.random() * this.subset.length); 1155 let w = this._generate_word(idx); 1156 1157 text.push(...w); 1158 text.push(this.get_subset(this.symbols).indexOf(" ")); 1159 } 1160 1161 return text.slice(0, text.length - 1); 1162 } 1163 1164 symbolize(seq) { 1165 let self = this; 1166 return seq.map(a => this.symbols[this.subset[a]]).join(""); 1167 } 1168 1169 matches(subset) { 1170 let subset_sorted = new Array(...subset).sort(); 1171 let me = this.get_subset(this.symbols).filter(x => x != " ").slice().sort(); 1172 return subset_sorted.join("") == me.join(""); 1173 } 1174 1175 next_text() { 1176 return {"text": this.symbolize(this._generate_text())}; 1177 } 1178 1179 set_data(data) { 1180 this.subset = new Array(...data["subset"]).map(x => this.symbols.indexOf(x)); 1181 this.subset.push(this.symbols.indexOf(" ")); 1182 if (data.hasOwnProperty("important")) 1183 this.important = new Array(...data["important"]).map(x => this.symbols.indexOf(x)); 1184 } 1185 1186 get_data() { 1187 let subset = new Array(...this.subset).map(x => this.symbols[x]).filter(x => x != " ").join(""); 1188 let important = this.symbolize(this.important.map(x => this.subset.indexOf(x))); 1189 return {"type": "markov", "subset": subset, "important": important}; 1190 } 1191 1192 text_completed(text_obj) {} 1193 1194 render_sidebar(sidebar, cb_save) { 1195 let table = document.createElement("div"); 1196 table.id = "morse-table"; 1197 1198 let col_left = document.createElement("div"); 1199 col_left.id = "col-left"; 1200 col_left.classList.add("morse-col"); 1201 1202 let col_mid = document.createElement("div"); 1203 col_mid.id = "col-mid"; 1204 1205 let col_right = document.createElement("div"); 1206 col_right.id = "col-right"; 1207 col_right.classList.add("morse-col"); 1208 1209 table.replaceChildren(col_left, col_mid, col_right); 1210 sidebar.replaceChildren(table); 1211 1212 let self = this; 1213 fill_morse_table(table, (el, morse_data) => { 1214 let [letter, _] = morse_data; 1215 let idx = this.symbols.indexOf(letter); 1216 1217 if (self.subset.indexOf(idx) === -1) { 1218 el.classList.add("morse-inactive"); 1219 } else if (self.important.indexOf(idx) !== -1) { 1220 el.classList.add("morse-important"); 1221 } 1222 1223 el.addEventListener("click", _ => { 1224 self.toggle(letter, false); 1225 self.render_sidebar(sidebar, cb_save); 1226 cb_save(); 1227 }); 1228 el.addEventListener("contextmenu", e => { 1229 e.preventDefault(); 1230 self.toggle(letter, true); 1231 self.render_sidebar(sidebar, cb_save); 1232 cb_save(); 1233 }); 1234 }); 1235 } 1236} 1237 1238 1239class QuoteLoader extends TextGenerator { 1240 constructor() { 1241 super(); 1242 this.quotes = []; 1243 } 1244 1245 describe() { 1246 return "Quote"; 1247 } 1248 1249 describe_short() { 1250 return "Quote"; 1251 } 1252 1253 _set_completed(completed_hashes) { 1254 localStorage.setItem("completed-quotes", JSON.stringify(completed_hashes)); 1255 } 1256 1257 _get_completed() { 1258 try { 1259 let completed = JSON.parse(localStorage.getItem("completed-quotes")); 1260 if (completed === null) { 1261 this._set_completed([]); 1262 return []; 1263 } 1264 return completed; 1265 } catch (SyntaxError) { 1266 this._set_completed([]); 1267 return []; 1268 } 1269 } 1270 1271 get_available_quotes() { 1272 let available_quotes = []; 1273 1274 let hashes = this._get_completed(); 1275 for (let quote of this.quotes) { 1276 let hash = strhash(quote["quote"]); 1277 if (hashes.indexOf(hash) === -1) { 1278 available_quotes.push(quote); 1279 } 1280 } 1281 1282 return available_quotes; 1283 } 1284 1285 async load_resources() { 1286 let self = this; 1287 1288 await fetch("data/quotes.json", { 1289 mode: "no-cors", 1290 headers: { 1291 "Content-Type": "application/json" 1292 }, 1293 }).then(data => 1294 data.json() 1295 ).then(data => { 1296 self.quotes = data; 1297 }); 1298 } 1299 1300 next_text() { 1301 let available = this.get_available_quotes(); 1302 if (available.length == 0) { 1303 // TODO: Alert user! 1304 this._set_completed([]); 1305 available = this.quotes; 1306 } 1307 1308 let quote = available[0|Math.random()*available.length]; 1309 return {"text": quote["quote"], "author": quote["author"]}; 1310 } 1311 1312 text_completed(text_obj) { 1313 let hashes = this._get_completed(); 1314 hashes.push(strhash(text_obj["text"])); 1315 this._set_completed(hashes); 1316 } 1317 1318 render_sidebar(sidebar, _cb_save) { 1319 let span = document.createElement("span"); 1320 let n_quotes = this.quotes.length; 1321 let n_completed = this._get_completed().length; 1322 span.innerText = `Completed ${n_completed}/${n_quotes} quotes`; 1323 1324 let real = document.createElement("span"); 1325 real.innerText = "[None of the quotes presented here are real]"; 1326 1327 let table = document.createElement("div"); 1328 table.id = "morse-table"; 1329 1330 let col_left = document.createElement("div"); 1331 col_left.id = "col-left"; 1332 col_left.classList.add("morse-col"); 1333 1334 let col_mid = document.createElement("div"); 1335 col_mid.id = "col-mid"; 1336 1337 let col_right = document.createElement("div"); 1338 col_right.id = "col-right"; 1339 col_right.classList.add("morse-col"); 1340 1341 table.replaceChildren(col_left, col_mid, col_right); 1342 1343 fill_morse_table(table, (el, morse_data) => {}); 1344 1345 sidebar.replaceChildren(span, table, document.createElement("br"), real); 1346 } 1347 1348 set_data(data) {} 1349 1350 get_data() { return { "type": "quotes" } } 1351} 1352 1353class SentenceLoader { 1354 constructor(sentence_config_el) { 1355 this.sentence_config_el = sentence_config_el; 1356 1357 // Are set in create_sidebar 1358 this.preset_selector_el = null; 1359 this.generator_sidebar = null; 1360 1361 this.current_generator = null; 1362 1363 this.sentence_up_to_date = true; 1364 } 1365 1366 current_data() { 1367 if (this.current_generator === null) { 1368 let data = localStorage.getItem("sentence-loader-data"); 1369 if (data !== null) { 1370 data = JSON.parse(data); 1371 } else { 1372 data = PRESETS[0]; 1373 } 1374 return data; 1375 } else { 1376 return this.current_generator.get_data(); 1377 } 1378 } 1379 1380 create_sidebar() { 1381 this.preset_selector_el = document.createElement("select"); 1382 1383 let children = []; 1384 1385 let found_match = false;
1386 1387 this.assert_generator(); 1388 let current_data = this.current_generator.get_data(); 1389 for (let preset of PRESETS) { 1390 if (preset == null) { 1391 let brk = document.createElement("option"); 1392 brk.disabled = true; 1393 brk.innerText = "â"; 1394 children.push(brk); 1395 continue; 1396 } 1397 1398 let option = document.createElement("option"); 1399 option.value = preset["id"]; 1400 option.innerText = preset["name"]; 1401 1402 let matches = true; 1403 for (let property of Object.getOwnPropertyNames(current_data)) { 1404 if (JSON.stringify(current_data[property]) !== JSON.stringify(preset[property])) { 1405 matches = false; 1406 } 1407 } 1408 1409 if (matches) { 1410 found_match = true; 1411 option.selected = true; 1412 } 1413 1414 children.push(option); 1415 } 1416 if (!found_match) { 1417 let brk = document.createElement("option"); 1418 brk.disabled = true; 1419 brk.innerText = "Custom"; 1420 brk.selected = true; 1421 children.push(brk); 1422 } 1423 this.preset_selector_el.replaceChildren(...children); 1424 1425 let self = this; 1426 this.preset_selector_el.addEventListener("change", e => { 1427 let chosen = e.target.value; 1428 for (let preset of PRESETS) { 1429 if (preset == null) 1430 continue; 1431 1432 if (preset["id"] == chosen) { 1433 self.set_data(preset); 1434 return; 1435 } 1436 } 1437 console.log("unknown value??"); 1438 console.log(chosen); 1439 }); 1440 1441 this.generator_sidebar = document.createElement("div"); 1442 1443 this.sentence_config_el.replaceChildren(this.preset_selector_el, this.generator_sidebar); 1444 } 1445 1446 async load_from_localstorage() { 1447 let data = PRESETS[0]; 1448 1449 let blob = localStorage.getItem("sentence-loader-data"); 1450 if (blob !== null) { 1451 data = JSON.parse(blob); 1452 } 1453 1454 await this.set_data(data).then(() => { 1455 this.create_sidebar(); 1456 this.redraw(); 1457 }); 1458 } 1459 1460 async set_data(data) { 1461 if (data["type"] === "quotes") { 1462 this.current_generator = new QuoteLoader(); 1463 } else if (data["type"] === "markov") { 1464 this.current_generator = new MarkovGenerator(); 1465 } else if (data["type"] === "hamgen") { 1466 this.current_generator = new HamGen(); 1467 } else { 1468 console.log("No such type " + data["type"]); 1469 return; 1470 } 1471 1472 this.sentence_up_to_date = false; 1473 1474 localStorage.setItem("sentence-loader-data", JSON.stringify(data)); 1475 1476 await this.current_generator.load_resources(); 1477 this.current_generator.set_data(data); 1478 1479 if (this.generator_sidebar !== null) 1480 this.redraw(); 1481 } 1482 1483 assert_generator() { 1484 if (this.current_generator == null) { 1485 throw "SentenceLoader function called before initialization. Please call this.load(state)/this.load_from_localstorage()"; 1486 } 1487 } 1488 1489 redraw() { 1490 let self = this; 1491 function cb_save() { 1492 let data = self.current_generator.get_data(); 1493 localStorage.setItem("sentence-loader-data", JSON.stringify(data)); 1494 1495 self.sentence_up_to_date = false; 1496 self.redraw(); 1497 } 1498 1499 this.assert_generator(); 1500 this.current_generator.render_sidebar(this.generator_sidebar, cb_save); 1501 1502 if (!this.sentence_up_to_date) { 1503 this.generator_sidebar.appendChild(document.createElement("br")); 1504 1505 let note = document.createElement("span"); 1506 note.classList.add("warning"); 1507 note.innerText = "Current sentence is from previous mode.\nGenerate a new sentence with <Tab>." 1508 this.generator_sidebar.appendChild(note); 1509 } 1510 } 1511 1512 completed(text) { 1513 this.current_generator.text_completed(text); 1514 this.redraw(); 1515 } 1516 1517 next_text() { 1518 this.sentence_up_to_date = true; 1519 this.redraw(); 1520 return this.current_generator.next_text(); 1521 } 1522}
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.