Refactoring for cleanliness and sanity
- Replaced 'var' with 'const'/'let' everywhere that made sense to. - Removed some duplicate code and tidied up a few functions. - Marked private functions with leading underscore. - Documentation and formatting improvements.
This commit is contained in:
parent
bd4451d924
commit
cc3c21b406
|
@ -0,0 +1 @@
|
|||
/.idea
|
591
index.html
591
index.html
|
@ -1,374 +1,399 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Visual Farnsworth CW Trainer</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script language="javascript" src="trainer.js"></script>
|
||||
</head>
|
||||
<body id="home">
|
||||
<a name="top"></a>
|
||||
<div id="player">
|
||||
<h1>A Visual Farnsworth CW Trainer</h1>
|
||||
<div id="links">
|
||||
<script type="application/javascript" src="trainer.js"></script>
|
||||
</head>
|
||||
<body id="home">
|
||||
<a id="top"></a>
|
||||
<div id="player">
|
||||
<h1>A Visual Farnsworth CW Trainer</h1>
|
||||
<div id="links">
|
||||
<a href="#about">About</a>
|
||||
<a href="#help">Help</a>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<div id="controls">
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<div id="controls">
|
||||
<div class="container">
|
||||
<h3>Elements</h3>
|
||||
<select class="select" id="wpmSelect">
|
||||
<option value="15">15 WPM</option>
|
||||
<option value="18">18 WPM</option>
|
||||
<option value="20">20 WPM</option>
|
||||
<option value="23">23 WPM</option>
|
||||
<option value="25">25 WPM</option>
|
||||
<option value="30">30 WPM</option>
|
||||
</select>
|
||||
<label for="wpmSelect">Elements</label>
|
||||
<select class="select" id="wpmSelect">
|
||||
<option value="15">15 WPM</option>
|
||||
<option value="18">18 WPM</option>
|
||||
<option value="20">20 WPM</option>
|
||||
<option value="23">23 WPM</option>
|
||||
<option value="25">25 WPM</option>
|
||||
<option value="30">30 WPM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>Farnsworth</h3>
|
||||
<select class="select" id="fwSelect">
|
||||
<option value="3">3 WPM</option>
|
||||
<option value="5">5 WPM</option>
|
||||
<option value="8">8 WPM</option>
|
||||
<option value="10">10 WPM</option>
|
||||
<option value="13">13 WPM</option>
|
||||
<option value="15">15 WPM</option>
|
||||
<option value="18">18 WPM</option>
|
||||
<option value="20">20 WPM</option>
|
||||
<option value="25">25 WPM</option>
|
||||
<option value="30">30 WPM</option>
|
||||
</select>
|
||||
<label for="fwSelect">Farnsworth</label>
|
||||
<select class="select" id="fwSelect">
|
||||
<option value="3">3 WPM</option>
|
||||
<option value="5">5 WPM</option>
|
||||
<option value="8">8 WPM</option>
|
||||
<option value="10">10 WPM</option>
|
||||
<option value="13">13 WPM</option>
|
||||
<option value="15">15 WPM</option>
|
||||
<option value="18">18 WPM</option>
|
||||
<option value="20">20 WPM</option>
|
||||
<option value="25">25 WPM</option>
|
||||
<option value="30">30 WPM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>Display</h3>
|
||||
<select class="select" id="displaySelect">
|
||||
<option value="immediate">Immediate</option>
|
||||
<option value="lingering">Lingering</option>
|
||||
<option value="delayed">Delayed</option>
|
||||
</select>
|
||||
<label for="displaySelect">Display</label>
|
||||
<select class="select" id="displaySelect">
|
||||
<option value="immediate">Immediate</option>
|
||||
<option value="lingering">Lingering</option>
|
||||
<option value="delayed">Delayed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>Type</h3>
|
||||
<select class="select" id="typeSelect">
|
||||
<option value="words">Top CW Words</option>
|
||||
<option value="random">Random Groups</option>
|
||||
</select>
|
||||
<label for="typeSelect">Type</label>
|
||||
<select class="select" id="typeSelect">
|
||||
<option value="words">Top CW Words</option>
|
||||
<option value="random">Random Groups</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="container cgroup" id="selectedType"></div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
<div id="sendText"></div>
|
||||
</div>
|
||||
<input type="button" id="textButton" value="New Text" />
|
||||
<input type="button" id="sendButton" value="Send" />
|
||||
<input type="button" id="cancelButton" value="Stop" disabled="true" />
|
||||
<div id="explainer">
|
||||
<a name="about"></a>
|
||||
</div>
|
||||
<input type="button" id="textButton" value="New Text"/>
|
||||
<input type="button" id="sendButton" value="Send"/>
|
||||
<input type="button" id="cancelButton" value="Stop" disabled/>
|
||||
<div id="explainer">
|
||||
<a id="about"></a>
|
||||
<h2>About</h2>
|
||||
<p>This trainer is a small personal project designed to play
|
||||
with learning morse code through the Farnsworth method.</p>
|
||||
|
||||
with learning morse code through the Farnsworth method.</p>
|
||||
|
||||
<p>The main difference between this trainer and others is that I
|
||||
am experimenting with visual reinforcement by displaying the
|
||||
character currently being sent. It is my hope that this will
|
||||
help form a stronger link between the <em>sound</em> of CW
|
||||
and the <em>meaning</em> of the sound.</p>
|
||||
am experimenting with visual reinforcement by displaying the
|
||||
character currently being sent. It is my hope that this will
|
||||
help form a stronger link between the <em>sound</em> of CW
|
||||
and the <em>meaning</em> of the sound.</p>
|
||||
|
||||
<p>This page should work in any modern desktop or mobile
|
||||
browser, including Edge, Safari, Chrome, Firefox, and
|
||||
Opera. But, please be aware that it does not work in
|
||||
Internet Explorer, because IE does not support the Web Audio
|
||||
API.</p>
|
||||
|
||||
browser, including Edge, Safari, Chrome, Firefox, and
|
||||
Opera. But, please be aware that it does not work in
|
||||
Internet Explorer, because IE does not support the Web Audio
|
||||
API.</p>
|
||||
|
||||
<a href="#top">Top</a>
|
||||
|
||||
<a name="help"></a>
|
||||
<a id="help"></a>
|
||||
<h2>Help</h2>
|
||||
<dl>
|
||||
<dt>New Text</dt>
|
||||
<dd>Generate a new practice text based on current settings</dd>
|
||||
<dt>New Text</dt>
|
||||
<dd>Generate a new practice text based on current settings</dd>
|
||||
|
||||
<dt>Send</dt>
|
||||
<dd>Start sending immediately</dd>
|
||||
<dt>Send</dt>
|
||||
<dd>Start sending immediately</dd>
|
||||
|
||||
<dt>Stop</dt>
|
||||
<dd>Stop sending immediately</dd>
|
||||
|
||||
<dt>Elements (Speed)</dt>
|
||||
<dd>Sets the speed used for the spacing of dits and dahs
|
||||
within each individual character. It is suggested that you
|
||||
not put this below 20WPM.</dd>
|
||||
<dt>Stop</dt>
|
||||
<dd>Stop sending immediately</dd>
|
||||
|
||||
<dt>Farnsworth (Speed)</dt>
|
||||
<dd>Sets the speed that will be used for spacing inbetween
|
||||
each character. Start slow, and gradually increase the
|
||||
Farnsworth speed until you have difficulty copying.</dd>
|
||||
<dt>Elements (Speed)</dt>
|
||||
<dd>Sets the speed used for the spacing of dits and dahs
|
||||
within each individual character. It is suggested that you
|
||||
not put this below 20WPM.
|
||||
</dd>
|
||||
|
||||
<dt>Display</dt>
|
||||
<dd>
|
||||
<p>There are three possible options for display mode.</p>
|
||||
<ol>
|
||||
<li><em>Immediate</em>: The character is displayed during
|
||||
the transmission of the character only, but hidden
|
||||
as soon as the character has been transmitted.</li>
|
||||
<li><em>Lingering</em>: The character is displayed during
|
||||
the transmission of the character, but will hang around
|
||||
until the next character is transmitted.</li>
|
||||
<li><em>Delayed</em>: The character will be displayed
|
||||
starting immediately <em>after</em> the character has
|
||||
been sent.</li>
|
||||
</ol>
|
||||
<dt>Farnsworth (Speed)</dt>
|
||||
<dd>Sets the speed that will be used for spacing inbetween
|
||||
each character. Start slow, and gradually increase the
|
||||
Farnsworth speed until you have difficulty copying.
|
||||
</dd>
|
||||
|
||||
<dt>Type</dt>
|
||||
<dd>Select between <em>Top CW Words</em>, which will display
|
||||
a random selection of the top 100 most common words, or
|
||||
<em>Random Groups</em>, which will send random groups of
|
||||
five characters each.</dd>
|
||||
<dt>Display</dt>
|
||||
<dd>
|
||||
<p>There are three possible options for display mode.</p>
|
||||
<ol>
|
||||
<li><em>Immediate</em>: The character is displayed during
|
||||
the transmission of the character only, but hidden
|
||||
as soon as the character has been transmitted.
|
||||
</li>
|
||||
<li><em>Lingering</em>: The character is displayed during
|
||||
the transmission of the character, but will hang around
|
||||
until the next character is transmitted.
|
||||
</li>
|
||||
<li><em>Delayed</em>: The character will be displayed
|
||||
starting immediately <em>after</em> the character has
|
||||
been sent.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<dt>Call Signs</dt>
|
||||
<dd>If checked, include randomly generated call signs in the
|
||||
<em>Top CW Words</em>.</dd>
|
||||
<dt>Type</dt>
|
||||
<dd>Select between <em>Top CW Words</em>, which will display
|
||||
a random selection of the top 100 most common words, or
|
||||
<em>Random Groups</em>, which will send random groups of
|
||||
five characters each.
|
||||
</dd>
|
||||
|
||||
<dt>Prosigns</dt>
|
||||
<dd>If checked, include prosigns in the <em>Top CW Words</em>.
|
||||
Prosigns include
|
||||
<strong>AR</strong> (“DISREGARD”),
|
||||
<strong>AS</strong> (“WAIT”),
|
||||
<strong>BT</strong> (“NEW PARAGRAPH”),
|
||||
<strong>SK</strong> (“END OF CONTACT”),
|
||||
<strong>KN</strong> (“GO AHEAD”), and
|
||||
<strong>BK</strong> (“BREAK”).</dd>
|
||||
<dt>Call Signs</dt>
|
||||
<dd>If checked, include randomly generated call signs in the
|
||||
<em>Top CW Words</em>.
|
||||
</dd>
|
||||
|
||||
<dt>Letters</dt>
|
||||
<dd>If checked, include the characters A–Z in the
|
||||
randomly generated character group.</dd>
|
||||
<dt>Prosigns</dt>
|
||||
<dd>If checked, include prosigns in the <em>Top CW Words</em>.
|
||||
Prosigns include
|
||||
<strong>AR</strong> (“DISREGARD”),
|
||||
<strong>AS</strong> (“WAIT”),
|
||||
<strong>BT</strong> (“NEW PARAGRAPH”),
|
||||
<strong>SK</strong> (“END OF CONTACT”),
|
||||
<strong>KN</strong> (“GO AHEAD”), and
|
||||
<strong>BK</strong> (“BREAK”).
|
||||
</dd>
|
||||
|
||||
<dt>Numbers</dt>
|
||||
<dd>If checked, include the characters 0–9 in the
|
||||
randomly generated character groups.</dd>
|
||||
<dt>Letters</dt>
|
||||
<dd>If checked, include the characters A–Z in the
|
||||
randomly generated character group.
|
||||
</dd>
|
||||
|
||||
<dt>Symbols</dt>
|
||||
<dd>If checked, include the characters “.”
|
||||
(period), “,” (comma), “?”
|
||||
(question mark), “=” (equal sign), and
|
||||
”/“ (slash) in the randomly generated
|
||||
character groups.</dd>
|
||||
<dt>Numbers</dt>
|
||||
<dd>If checked, include the characters 0–9 in the
|
||||
randomly generated character groups.
|
||||
</dd>
|
||||
|
||||
<dt>Symbols</dt>
|
||||
<dd>If checked, include the characters “.”
|
||||
(period), “,” (comma), “?”
|
||||
(question mark), “=” (equal sign), and
|
||||
”/“ (slash) in the randomly generated
|
||||
character groups.
|
||||
</dd>
|
||||
</dl>
|
||||
<a href="#top">Top</a>
|
||||
|
||||
</div>
|
||||
<div id="footer">
|
||||
|
||||
</div>
|
||||
<div id="footer">
|
||||
Copyright © 2019, Seth Morabito <web@loomcom.com>.
|
||||
|
||||
Licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script language="javascript">
|
||||
const DEFAULT_WPM = 20;
|
||||
const DEFAULT_FARNSWORTH = 5;
|
||||
const DEFAULT_GROUP_COUNT = 50;
|
||||
const DEFAULT_GROUP_LEN = 5;
|
||||
|
||||
const wordsBlock = `
|
||||
<script type="application/javascript">
|
||||
const DEFAULT_WPM = 20;
|
||||
const DEFAULT_FARNSWORTH = 8;
|
||||
const wordsBlock = `
|
||||
<div class="container">
|
||||
<h3>Call Signs</h3>
|
||||
<label for="callsignsCheckbox">Call Signs</label>
|
||||
<input type="checkbox" id="callsignsCheckbox" />
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>Prosigns</h3>
|
||||
<label for="prosignsCheckbox">Prosigns</label>
|
||||
<input type="checkbox" id="prosignsCheckbox" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
const groupsBlock = `
|
||||
const groupsBlock = `
|
||||
<div class="container">
|
||||
<h3>Letters</h3>
|
||||
<label for="lettersCheckbox">Letters</label>
|
||||
<input type="checkbox" id="lettersCheckbox" />
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>Numbers</h3>
|
||||
<label for="numbersCheckbox">Numbers</label>
|
||||
<input type="checkbox" id="numbersCheckbox" />
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>Symbols</h3>
|
||||
<label for="numbersCheckbox">Symbols</label>
|
||||
<input type="checkbox" id="symbolsCheckbox" />
|
||||
</div>
|
||||
`;
|
||||
var text;
|
||||
`;
|
||||
|
||||
var charNum = 0;
|
||||
const wpmSelect = $('wpmSelect');
|
||||
const fwSelect = $('fwSelect');
|
||||
const typeSelect = $('typeSelect');
|
||||
const textButton = $('textButton');
|
||||
const sendButton = $('sendButton');
|
||||
const cancelButton = $('cancelButton');
|
||||
const sendText = $('sendText');
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
let text;
|
||||
let charNum = 0;
|
||||
|
||||
function disableUi() {
|
||||
const type = $("typeSelect").value;
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
$("wpmSelect").disabled = true;
|
||||
$("fwSelect").disabled = true;
|
||||
$("textButton").disabled = true;
|
||||
$("sendButton").disabled = true;
|
||||
$("cancelButton").disabled = false;
|
||||
$("sendText").disabled = true;
|
||||
$("typeSelect").disabled = true;
|
||||
if (type === 'words') {
|
||||
$("callsignsCheckbox").disabled = true;
|
||||
$("prosignsCheckbox").disabled = true;
|
||||
} else {
|
||||
$("lettersCheckbox").disabled = true;
|
||||
$("numbersCheckbox").disabled = true;
|
||||
$("symbolsCheckbox").disabled = true;
|
||||
}
|
||||
}
|
||||
function disableUi() {
|
||||
const type = typeSelect.value;
|
||||
wpmSelect.disabled = true;
|
||||
fwSelect.disabled = true;
|
||||
textButton.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
cancelButton.disabled = false;
|
||||
sendText.disabled = true;
|
||||
typeSelect.disabled = true;
|
||||
if (type === 'words') {
|
||||
$('callsignsCheckbox').disabled = true;
|
||||
$('prosignsCheckbox').disabled = true;
|
||||
} else {
|
||||
$('lettersCheckbox').disabled = true;
|
||||
$('numbersCheckbox').disabled = true;
|
||||
$('symbolsCheckbox').disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function enableUi() {
|
||||
const type = $("typeSelect").value;
|
||||
function enableUi() {
|
||||
const type = typeSelect.value;
|
||||
$('output').innerHTML = '';
|
||||
wpmSelect.disabled = false;
|
||||
fwSelect.disabled = false;
|
||||
textButton.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
cancelButton.disabled = true;
|
||||
sendText.disabled = false;
|
||||
typeSelect.disabled = false;
|
||||
if (type === 'words') {
|
||||
$('callsignsCheckbox').disabled = false;
|
||||
$('prosignsCheckbox').disabled = false;
|
||||
} else {
|
||||
$('lettersCheckbox').disabled = false;
|
||||
$('numbersCheckbox').disabled = false;
|
||||
$('symbolsCheckbox').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
$("output").innerHTML = "";
|
||||
$("wpmSelect").disabled = false;
|
||||
$("fwSelect").disabled = false;
|
||||
$("textButton").disabled = false;
|
||||
$("sendButton").disabled = false;
|
||||
$("cancelButton").disabled = true;
|
||||
$("sendText").disabled = false;
|
||||
$("typeSelect").disabled = false;
|
||||
if (type === 'words') {
|
||||
$("callsignsCheckbox").disabled = false;
|
||||
$("prosignsCheckbox").disabled = false;
|
||||
} else {
|
||||
$("lettersCheckbox").disabled = false;
|
||||
$("numbersCheckbox").disabled = false;
|
||||
$("symbolsCheckbox").disabled = false;
|
||||
}
|
||||
}
|
||||
function updateConfig() {
|
||||
const selectedType = $('selectedType');
|
||||
|
||||
function updateConfig() {
|
||||
const type = $("typeSelect").value;
|
||||
if (type === 'words') {
|
||||
$("selectedType").innerHTML = wordsBlock;
|
||||
const type = $("typeSelect").value;
|
||||
|
||||
$("callsignsCheckbox").checked = trainer.enableCallsigns;
|
||||
$("prosignsCheckbox").checked = trainer.enableProsigns;
|
||||
if (type === 'words') {
|
||||
selectedType.innerHTML = wordsBlock;
|
||||
|
||||
$("callsignsCheckbox").onclick = function() {
|
||||
trainer.enableCallsigns = $("callsignsCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
$("prosignsCheckbox").onclick = function() {
|
||||
trainer.enableProsigns = $("prosignsCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
} else {
|
||||
$("selectedType").innerHTML = groupsBlock;
|
||||
const callsignsCheckbox = $('callsignsCheckbox');
|
||||
const prosignsCheckbox = $('prosignsCheckbox');
|
||||
|
||||
$("lettersCheckbox").checked = trainer.enableLetters;
|
||||
$("numbersCheckbox").checked = trainer.enableNumbers;
|
||||
$("symbolsCheckbox").checked = trainer.enableSymbols;
|
||||
callsignsCheckbox.checked = trainer.enableCallsigns;
|
||||
prosignsCheckbox.checked = trainer.enableProsigns;
|
||||
|
||||
$("lettersCheckbox").onclick = function() {
|
||||
trainer.enableLetters = $("lettersCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
$("numbersCheckbox").onclick = function() {
|
||||
trainer.enableNumbers = $("numbersCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
$("symbolsCheckbox").onclick = function() {
|
||||
trainer.enableSymbols = $("symbolsCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
}
|
||||
callsignsCheckbox.onclick = function () {
|
||||
trainer.enableCallsigns = $("callsignsCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
prosignsCheckbox.onclick = function () {
|
||||
trainer.enableProsigns = $("prosignsCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
} else {
|
||||
selectedType.innerHTML = groupsBlock;
|
||||
|
||||
generateText();
|
||||
}
|
||||
const lettersCheckbox = $('lettersCheckbox');
|
||||
const numbersCheckbox = $('numbersCheckbox');
|
||||
const symbolsCheckbox = $('symbolsCheckbox');
|
||||
|
||||
function showChar(c) {
|
||||
var newText = text.replace(/@/g, '');
|
||||
lettersCheckbox.checked = trainer.enableLetters;
|
||||
numbersCheckbox.checked = trainer.enableNumbers;
|
||||
symbolsCheckbox.checked = trainer.enableSymbols;
|
||||
|
||||
charNum += c.length;
|
||||
lettersCheckbox.onclick = function () {
|
||||
trainer.enableLetters = $("lettersCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
numbersCheckbox.onclick = function () {
|
||||
trainer.enableNumbers = $("numbersCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
symbolsCheckbox.onclick = function () {
|
||||
trainer.enableSymbols = $("symbolsCheckbox").checked;
|
||||
updateConfig();
|
||||
};
|
||||
}
|
||||
|
||||
if ($("displaySelect").value === "delayed") {
|
||||
$("output").innerHTML = "";
|
||||
} else {
|
||||
$("output").innerHTML = c;
|
||||
}
|
||||
generateText();
|
||||
}
|
||||
|
||||
newText =
|
||||
"<span class=\"hilighted\">" + newText.substring(0, charNum) + "</span>" +
|
||||
newText.substring(charNum, newText.length);
|
||||
function showChar(c) {
|
||||
let newText = text.replace(/@/g, '');
|
||||
|
||||
if (text[charNum + 1] === ' ') {
|
||||
charNum++;
|
||||
}
|
||||
charNum += c.length;
|
||||
|
||||
$("sendText").innerHTML = newText;
|
||||
}
|
||||
if ($("displaySelect").value === "delayed") {
|
||||
$("output").innerHTML = "";
|
||||
} else {
|
||||
$("output").innerHTML = c;
|
||||
}
|
||||
|
||||
function hideChar(c) {
|
||||
if ($("displaySelect").value === "delayed") {
|
||||
$("output").innerHTML = c;
|
||||
} else if ($("displaySelect").value === "immediate") {
|
||||
$("output").innerHTML = "";
|
||||
}
|
||||
}
|
||||
newText =
|
||||
"<span class=\"hilighted\">" + newText.substring(0, charNum) + "</span>" +
|
||||
newText.substring(charNum, newText.length);
|
||||
|
||||
$("wpmSelect").value = DEFAULT_WPM;
|
||||
$("fwSelect").value = DEFAULT_FARNSWORTH;
|
||||
if (text[charNum + 1] === ' ') {
|
||||
charNum++;
|
||||
}
|
||||
|
||||
let trainer = new CwTrainer(DEFAULT_WPM,
|
||||
DEFAULT_FARNSWORTH,
|
||||
showChar,
|
||||
hideChar,
|
||||
enableUi,
|
||||
enableUi);
|
||||
sendText.innerHTML = newText;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
trainer.cancel();
|
||||
}
|
||||
function hideChar(c) {
|
||||
const displaySelect = $('displaySelect');
|
||||
if (displaySelect.value === "delayed") {
|
||||
$("output").innerHTML = c;
|
||||
} else if (displaySelect.value === "immediate") {
|
||||
$("output").innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function generateText() {
|
||||
charNum = 0;
|
||||
wpmSelect.value = DEFAULT_WPM;
|
||||
fwSelect.value = DEFAULT_FARNSWORTH;
|
||||
|
||||
if ($("typeSelect").value === 'words') {
|
||||
text = trainer.randomText(75);
|
||||
} else {
|
||||
text = trainer.randomGroups(60, 5);
|
||||
}
|
||||
|
||||
$("sendText").innerHTML =
|
||||
text.replace(/@/g, '');
|
||||
}
|
||||
let trainer = new CwTrainer(DEFAULT_WPM,
|
||||
DEFAULT_FARNSWORTH,
|
||||
showChar,
|
||||
hideChar,
|
||||
enableUi,
|
||||
enableUi);
|
||||
|
||||
function send() {
|
||||
// Webkit requires that a user interaction unsuspend
|
||||
// the audio context, otherwise no audio.
|
||||
trainer.unsuspend();
|
||||
disableUi();
|
||||
charNum = 0;
|
||||
trainer.sendText(text);
|
||||
}
|
||||
function cancel() {
|
||||
trainer.cancel();
|
||||
}
|
||||
|
||||
function setWpm() {
|
||||
trainer.setWpm($("wpmSelect").value,
|
||||
$("fwSelect").value);
|
||||
}
|
||||
function generateText() {
|
||||
charNum = 0;
|
||||
|
||||
$("wpmSelect").onchange = setWpm;
|
||||
$("fwSelect").onchange = setWpm;
|
||||
$("typeSelect").onchange = updateConfig;
|
||||
if (typeSelect.value === 'words') {
|
||||
text = trainer.randomText(75);
|
||||
} else {
|
||||
text = trainer.randomGroups(60, 5);
|
||||
}
|
||||
|
||||
$("textButton").onclick = generateText;
|
||||
$("sendButton").onclick = send;
|
||||
$("cancelButton").onclick = cancel;
|
||||
$("sendText").innerHTML =
|
||||
text.replace(/@/g, '');
|
||||
}
|
||||
|
||||
updateConfig();
|
||||
generateText();
|
||||
</script>
|
||||
</body>
|
||||
function send() {
|
||||
// Webkit requires that a user interaction unsuspend
|
||||
// the audio context, otherwise no audio.
|
||||
trainer.unsuspend();
|
||||
disableUi();
|
||||
charNum = 0;
|
||||
trainer.sendText(text);
|
||||
}
|
||||
|
||||
function setWpm() {
|
||||
trainer.setWpm($("wpmSelect").value,
|
||||
$("fwSelect").value);
|
||||
}
|
||||
|
||||
wpmSelect.onchange = setWpm;
|
||||
fwSelect.onchange = setWpm;
|
||||
typeSelect.onchange = updateConfig;
|
||||
|
||||
textButton.onclick = generateText;
|
||||
sendButton.onclick = send;
|
||||
cancelButton.onclick = cancel;
|
||||
|
||||
updateConfig();
|
||||
generateText();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -74,7 +74,10 @@ a {
|
|||
font-size: 16pt;
|
||||
}
|
||||
|
||||
.container h3 {
|
||||
.container label {
|
||||
display: block;
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0 8px 0 8px;
|
||||
}
|
||||
|
@ -94,10 +97,6 @@ input[type=checkbox] {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.container h3 {
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
.bigProsign, .prosign {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
|
281
trainer.js
281
trainer.js
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
// A Visual Farnsworth CW Trainer.
|
||||
//
|
||||
// Copyright (c) 2019, Seth Morabito <web@loomcom.com>
|
||||
// Copyright (c) 2019, 2020, Seth Morabito <web@loomcom.com>
|
||||
//
|
||||
// This software is licensed under the terms of the GNU Affero GPL
|
||||
// version 3.0. Please see the file LICENSE.txt for details.
|
||||
|
@ -123,26 +123,25 @@ let CwTrainer = (function () {
|
|||
'PWR', 'WX', '73', '5NN', '599', 'U', 'BTU', 'TST'
|
||||
];
|
||||
|
||||
PROSIGN_LIST = [
|
||||
const PROSIGN_LIST = [
|
||||
'@AR', '@BT', '@SK', '@KN', '@BK'
|
||||
];
|
||||
|
||||
var fwWpm;
|
||||
var audioContext;
|
||||
var oscNode;
|
||||
var gainNode;
|
||||
var time;
|
||||
var dotWidth;
|
||||
var dashWidth;
|
||||
var charSpace;
|
||||
var wordSpace;
|
||||
let audioContext;
|
||||
let oscNode;
|
||||
let gainNode;
|
||||
let time;
|
||||
let dotWidth;
|
||||
let dashWidth;
|
||||
let charSpace;
|
||||
let wordSpace;
|
||||
|
||||
var beforeCharCallback;
|
||||
var afterCharCallback;
|
||||
var afterSendCallback;
|
||||
var afterCancelCallback;
|
||||
let beforeCharCallback;
|
||||
let afterCharCallback;
|
||||
let afterSendCallback;
|
||||
let afterCancelCallback;
|
||||
|
||||
var pendingTimeouts = [];
|
||||
let pendingTimeouts = [];
|
||||
|
||||
class CwTrainer {
|
||||
|
||||
|
@ -166,7 +165,7 @@ let CwTrainer = (function () {
|
|||
afterSendCallback = afterSendCb;
|
||||
afterCancelCallback = afterCancelCb;
|
||||
|
||||
var AudioContext = (window.AudioContext ||
|
||||
let AudioContext = (window.AudioContext ||
|
||||
window.webkitAudioContext ||
|
||||
false);
|
||||
|
||||
|
@ -193,11 +192,20 @@ let CwTrainer = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
//
|
||||
// Unsuspend sending
|
||||
//
|
||||
unsuspend() {
|
||||
audioContext.resume();
|
||||
}
|
||||
|
||||
//
|
||||
// Set the Words per Minute to be used by this trainer.
|
||||
//
|
||||
setWpm(wpm, fw) {
|
||||
let fwDotWidth = 1.2 / fw;
|
||||
|
||||
|
@ -208,30 +216,15 @@ let CwTrainer = (function () {
|
|||
wordSpace = fwDotWidth * 7.0;
|
||||
}
|
||||
|
||||
makeCallSign() {
|
||||
var callsign = CALLPREFIXES[Math.floor(Math.random() * CALLPREFIXES.length)];
|
||||
|
||||
callsign += NUMBERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
|
||||
callsign += LETTERS[Math.floor(Math.random() * LETTERS.length)];
|
||||
|
||||
if (Math.random() > 0.5) {
|
||||
callsign += LETTERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
}
|
||||
|
||||
if (Math.random() > 0.5) {
|
||||
callsign += LETTERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
}
|
||||
|
||||
return callsign;
|
||||
}
|
||||
|
||||
//
|
||||
// Generate random text based on the most common words
|
||||
//
|
||||
randomText(numWords) {
|
||||
var words = [];
|
||||
|
||||
for (var i = 0; i < numWords; i++) {
|
||||
let words = [];
|
||||
|
||||
for (let i = 0; i < numWords; i++) {
|
||||
if (Math.random() < 0.05 && this.enableCallsigns) {
|
||||
words.push(this.makeCallSign());
|
||||
words.push(this._makeCallSign());
|
||||
} else if (Math.random() < 0.05 && this.enableProsigns) {
|
||||
words.push(
|
||||
PROSIGN_LIST[Math.floor(Math.random() * PROSIGN_LIST.length)]
|
||||
|
@ -245,10 +238,13 @@ let CwTrainer = (function () {
|
|||
|
||||
return words.join(" ");
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Generate random groups of characters
|
||||
//
|
||||
randomGroups(numGroups, groupSize) {
|
||||
var groups = [];
|
||||
var alphabet = [];
|
||||
let groups = [];
|
||||
let alphabet = [];
|
||||
|
||||
if (this.enableLetters) {
|
||||
alphabet = alphabet.concat(LETTERS);
|
||||
|
@ -265,12 +261,12 @@ let CwTrainer = (function () {
|
|||
if (alphabet.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (var i = 0; i < numGroups; i++) {
|
||||
var group = "";
|
||||
|
||||
for (var j = 0; j < groupSize; j++) {
|
||||
var c = alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
|
||||
for (let i = 0; i < numGroups; i++) {
|
||||
let group = "";
|
||||
|
||||
for (let j = 0; j < groupSize; j++) {
|
||||
let c = alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
group = group + c;
|
||||
}
|
||||
|
||||
|
@ -280,97 +276,19 @@ let CwTrainer = (function () {
|
|||
return groups.join(" ");
|
||||
}
|
||||
|
||||
sendMorseString(str) {
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var e = str[i];
|
||||
if (e === '.') {
|
||||
gainNode.gain.setValueAtTime(OFF, time);
|
||||
gainNode.gain.exponentialRampToValueAtTime(ON, time + RAMP);
|
||||
gainNode.gain.setValueAtTime(ON, time + dotWidth);
|
||||
gainNode.gain.exponentialRampToValueAtTime(OFF, time + dotWidth + RAMP);
|
||||
time = time + dotWidth + RAMP;
|
||||
} else if (e === '-') {
|
||||
gainNode.gain.setValueAtTime(OFF, time);
|
||||
gainNode.gain.exponentialRampToValueAtTime(ON, time + RAMP);
|
||||
gainNode.gain.setValueAtTime(ON, time + dashWidth);
|
||||
gainNode.gain.exponentialRampToValueAtTime(OFF, time + dashWidth + RAMP);
|
||||
time = time + dashWidth + RAMP;
|
||||
}
|
||||
if (i < str.length - 1) {
|
||||
time = time + dotWidth + RAMP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendChar(c) {
|
||||
var morseValue = CHARS[c];
|
||||
|
||||
if (beforeCharCallback) {
|
||||
pendingTimeouts.push(setTimeout(function() {
|
||||
beforeCharCallback(c);
|
||||
}, (time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
|
||||
if (morseValue) {
|
||||
this.sendMorseString(morseValue);
|
||||
}
|
||||
|
||||
if (afterCharCallback) {
|
||||
pendingTimeouts.push(setTimeout(function() {
|
||||
afterCharCallback(c);
|
||||
}, (time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
}
|
||||
|
||||
sendProsign(prosign) {
|
||||
if (prosign.startsWith('@')) {
|
||||
prosign = prosign.substring(1, prosign.length);
|
||||
}
|
||||
|
||||
var morseValue = PROSIGNS[prosign];
|
||||
|
||||
if (beforeCharCallback) {
|
||||
pendingTimeouts.push(setTimeout(function() {
|
||||
beforeCharCallback(prosign);
|
||||
}, (time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
|
||||
if (morseValue) {
|
||||
this.sendMorseString(morseValue);
|
||||
}
|
||||
|
||||
if (afterCharCallback) {
|
||||
pendingTimeouts.push(setTimeout(function() {
|
||||
afterCharCallback(prosign);
|
||||
}, (time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
}
|
||||
|
||||
sendWord(word) {
|
||||
if (word.startsWith('@')) {
|
||||
// Any word starting with @ is a prosign.
|
||||
this.sendProsign(word);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < word.length; i++) {
|
||||
this.sendChar(word[i].toUpperCase());
|
||||
if (i < word.length - 1) {
|
||||
time = time + charSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Send a full text
|
||||
//
|
||||
sendText(text) {
|
||||
// Add a small 1/2 second delay after the send button
|
||||
// is clicked.
|
||||
gainNode.gain.setValueAtTime(OFF, audioContext.currentTime);
|
||||
time = audioContext.currentTime + 0.5;
|
||||
|
||||
var words = text.split(" ");
|
||||
let words = text.split(' ');
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
this.sendWord(words[i]);
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
this._sendWord(words[i]);
|
||||
if (i < words.length - 1) {
|
||||
time = time + wordSpace;
|
||||
}
|
||||
|
@ -378,16 +296,19 @@ let CwTrainer = (function () {
|
|||
|
||||
if (afterSendCallback) {
|
||||
pendingTimeouts.push(setTimeout(afterSendCallback,
|
||||
(time - audioContext.currentTime) * 1000.0));
|
||||
(time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Suspend sending immediately
|
||||
//
|
||||
cancel() {
|
||||
gainNode.gain.cancelScheduledValues(audioContext.currentTime);
|
||||
gainNode.gain.setValueAtTime(OFF, audioContext.currentTime);
|
||||
time = 0.0;
|
||||
|
||||
for (var i = pendingTimeouts.length - 1; i >= 0; i--) {
|
||||
for (let i = pendingTimeouts.length - 1; i >= 0; i--) {
|
||||
window.clearTimeout(pendingTimeouts[i]);
|
||||
pendingTimeouts.pop();
|
||||
}
|
||||
|
@ -396,7 +317,97 @@ let CwTrainer = (function () {
|
|||
afterCancelCallback();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Private functions
|
||||
//
|
||||
|
||||
_makeCallSign() {
|
||||
let callsign = CALLPREFIXES[Math.floor(Math.random() * CALLPREFIXES.length)];
|
||||
|
||||
callsign += NUMBERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
|
||||
callsign += LETTERS[Math.floor(Math.random() * LETTERS.length)];
|
||||
|
||||
if (Math.random() > 0.5) {
|
||||
callsign += LETTERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
}
|
||||
|
||||
if (Math.random() > 0.5) {
|
||||
callsign += LETTERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
}
|
||||
|
||||
return callsign;
|
||||
}
|
||||
|
||||
//
|
||||
// Send an individual element, either a dot or a dash.
|
||||
//
|
||||
_sendDotOrDash(width) {
|
||||
gainNode.gain.setValueAtTime(OFF, time);
|
||||
gainNode.gain.exponentialRampToValueAtTime(ON, time + RAMP);
|
||||
gainNode.gain.setValueAtTime(ON, time + width);
|
||||
gainNode.gain.exponentialRampToValueAtTime(OFF, time + width + RAMP);
|
||||
time = time + width + RAMP;
|
||||
}
|
||||
|
||||
//
|
||||
// Send a list of dots and dashes
|
||||
//
|
||||
_sendMorseString(str) {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let e = str[i];
|
||||
if (e === '.') {
|
||||
this._sendDotOrDash(dotWidth);
|
||||
} else if (e === '-') {
|
||||
this._sendDotOrDash(dashWidth)
|
||||
}
|
||||
if (i < str.length - 1) {
|
||||
time = time + dotWidth + RAMP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Send an individual ASCII character or Prosign
|
||||
//
|
||||
_doSend(morseValue, val) {
|
||||
if (beforeCharCallback) {
|
||||
pendingTimeouts.push(setTimeout(function() {
|
||||
beforeCharCallback(val);
|
||||
}, (time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
|
||||
if (morseValue) {
|
||||
this._sendMorseString(morseValue);
|
||||
}
|
||||
|
||||
if (afterCharCallback) {
|
||||
pendingTimeouts.push(setTimeout(function() {
|
||||
afterCharCallback(val);
|
||||
}, (time - audioContext.currentTime) * 1000.0));
|
||||
}
|
||||
}
|
||||
|
||||
_sendWord(wordOrProsign) {
|
||||
if (wordOrProsign.startsWith('@')) {
|
||||
// Any word starting with @ is a prosign.
|
||||
if (wordOrProsign.startsWith('@')) {
|
||||
wordOrProsign = wordOrProsign.substring(1, wordOrProsign.length);
|
||||
}
|
||||
|
||||
this._doSend(PROSIGNS[wordOrProsign], wordOrProsign);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < wordOrProsign.length; i++) {
|
||||
this._doSend(CHARS[wordOrProsign[i].toUpperCase()], wordOrProsign[i].toUpperCase());
|
||||
if (i < wordOrProsign.length - 1) {
|
||||
time = time + charSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return CwTrainer;
|
||||
})();
|
||||
|
|
Loading…
Reference in New Issue