cwtrainer/index.html

400 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Visual Farnsworth CW Trainer</title>
<link rel="stylesheet" href="style.css">
<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 class="container">
<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">
<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">
<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">
<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 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/>
<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>
<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>
<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>
<a href="#top">Top</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>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>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>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>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>Call Signs</dt>
<dd>If checked, include randomly generated call signs in the
<em>Top CW Words</em>.
</dd>
<dt>Prosigns</dt>
<dd>If checked, include prosigns in the <em>Top CW Words</em>.
Prosigns include
<strong>AR</strong> (&ldquo;DISREGARD&rdquo;),
<strong>AS</strong> (&ldquo;WAIT&rdquo;),
<strong>BT</strong> (&ldquo;NEW PARAGRAPH&rdquo;),
<strong>SK</strong> (&ldquo;END OF CONTACT&rdquo;),
<strong>KN</strong> (&ldquo;GO AHEAD&rdquo;), and
<strong>BK</strong> (&ldquo;BREAK&rdquo;).
</dd>
<dt>Letters</dt>
<dd>If checked, include the characters A&ndash;Z in the
randomly generated character group.
</dd>
<dt>Numbers</dt>
<dd>If checked, include the characters 0&ndash;9 in the
randomly generated character groups.
</dd>
<dt>Symbols</dt>
<dd>If checked, include the characters &ldquo;.&rdquo;
(period), &ldquo;,&rdquo; (comma), &ldquo;?&rdquo;
(question mark), &ldquo;=&rdquo; (equal sign), and
&rdquo;/&ldquo; (slash) in the randomly generated
character groups.
</dd>
</dl>
<a href="#top">Top</a>
</div>
<div id="footer">
Copyright &copy; 2019, Seth Morabito &lt;web@loomcom.com&gt;.
Licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3</a>.
</div>
</div>
<script type="application/javascript">
const DEFAULT_WPM = 20;
const DEFAULT_FARNSWORTH = 8;
const wordsBlock = `
<div class="container">
<label for="callsignsCheckbox">Call Signs</label>
<input type="checkbox" id="callsignsCheckbox" />
</div>
<div class="container">
<label for="prosignsCheckbox">Prosigns</label>
<input type="checkbox" id="prosignsCheckbox" />
</div>
`;
const groupsBlock = `
<div class="container">
<label for="lettersCheckbox">Letters</label>
<input type="checkbox" id="lettersCheckbox" />
</div>
<div class="container">
<label for="numbersCheckbox">Numbers</label>
<input type="checkbox" id="numbersCheckbox" />
</div>
<div class="container">
<label for="numbersCheckbox">Symbols</label>
<input type="checkbox" id="symbolsCheckbox" />
</div>
`;
const wpmSelect = $('wpmSelect');
const fwSelect = $('fwSelect');
const typeSelect = $('typeSelect');
const textButton = $('textButton');
const sendButton = $('sendButton');
const cancelButton = $('cancelButton');
const sendText = $('sendText');
let text;
let charNum = 0;
function $(id) {
return document.getElementById(id);
}
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;
$('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');
const type = $("typeSelect").value;
if (type === 'words') {
selectedType.innerHTML = wordsBlock;
const callsignsCheckbox = $('callsignsCheckbox');
const prosignsCheckbox = $('prosignsCheckbox');
callsignsCheckbox.checked = trainer.enableCallsigns;
prosignsCheckbox.checked = trainer.enableProsigns;
callsignsCheckbox.onclick = function () {
trainer.enableCallsigns = $("callsignsCheckbox").checked;
updateConfig();
};
prosignsCheckbox.onclick = function () {
trainer.enableProsigns = $("prosignsCheckbox").checked;
updateConfig();
};
} else {
selectedType.innerHTML = groupsBlock;
const lettersCheckbox = $('lettersCheckbox');
const numbersCheckbox = $('numbersCheckbox');
const symbolsCheckbox = $('symbolsCheckbox');
lettersCheckbox.checked = trainer.enableLetters;
numbersCheckbox.checked = trainer.enableNumbers;
symbolsCheckbox.checked = trainer.enableSymbols;
lettersCheckbox.onclick = function () {
trainer.enableLetters = $("lettersCheckbox").checked;
updateConfig();
};
numbersCheckbox.onclick = function () {
trainer.enableNumbers = $("numbersCheckbox").checked;
updateConfig();
};
symbolsCheckbox.onclick = function () {
trainer.enableSymbols = $("symbolsCheckbox").checked;
updateConfig();
};
}
generateText();
}
function showChar(c) {
let newText = text.replace(/@/g, '');
charNum += c.length;
if ($("displaySelect").value === "delayed") {
$("output").innerHTML = "";
} else {
$("output").innerHTML = c;
}
if (newText[charNum] === ' ') {
charNum++;
}
newText =
"<span class=\"hilighted\">" + newText.substring(0, charNum) + "</span>" +
newText.substring(charNum, newText.length);
sendText.innerHTML = newText;
}
function hideChar(c) {
const displaySelect = $('displaySelect');
if (displaySelect.value === "delayed") {
$("output").innerHTML = c;
} else if (displaySelect.value === "immediate") {
$("output").innerHTML = "";
}
}
wpmSelect.value = DEFAULT_WPM;
fwSelect.value = DEFAULT_FARNSWORTH;
let trainer = new CwTrainer(DEFAULT_WPM,
DEFAULT_FARNSWORTH,
showChar,
hideChar,
enableUi,
enableUi);
function cancel() {
trainer.cancel();
}
function generateText() {
charNum = 0;
if (typeSelect.value === 'words') {
text = trainer.randomText(75);
} else {
text = trainer.randomGroups(60, 5);
}
$("sendText").innerHTML =
text.replace(/@/g, '');
}
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>