400 lines
13 KiB
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> (“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>Letters</dt>
|
|
<dd>If checked, include the characters A–Z in the
|
|
randomly generated character group.
|
|
</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">
|
|
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>
|
|
|
|
|
|
<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>
|