Neale Pickett
·
2024-10-21
inputs.mjs
1class Input {
2 constructor(keyer) {
3 this.keyer = keyer
4 }
5
6 SetDitDuration(delay) {
7 // Nothing
8 }
9
10 SetKeyerMode(mode) {
11 // Nothing
12 }
13}
14
15export class HTML extends Input{
16 constructor(keyer) {
17 super(keyer)
18
19 // Listen to HTML buttons
20 for (let e of document.querySelectorAll("button.key")) {
21 // Chrome is going to suggest you use passive events here.
22 // I tried that and it screws up Safari mobile,
23 // making it so that hitting the button selects text on the page.
24 e.addEventListener("contextmenu", e => { e.preventDefault(); return false }, {passive: false})
25 e.addEventListener("touchstart", e => this.keyButton(e), {passive: false})
26 e.addEventListener("touchend", e => this.keyButton(e), {passive: false})
27 e.addEventListener("mousedown", e => this.keyButton(e), {passive: false})
28 e.addEventListener("mouseup", e => this.keyButton(e), {passive: false})
29 e.contentEditable = false
30 }
31 }
32
33 keyButton(event) {
34 let down = event.type.endsWith("down") || event.type.endsWith("start")
35 let key = Number(event.target.dataset.key)
36
37 // Button 2 does the other key (assuming 2 keys)
38 if (event.button == 2) {
39 key = 1 - key
40 }
41 this.keyer.Key(key, down)
42
43 if (event.cancelable) {
44 event.preventDefault()
45 }
46 }
47}
48
49export class Keyboard extends Input{
50 constructor(keyer) {
51 super(keyer)
52
53 // Listen for keystrokes
54 document.addEventListener("keydown", e => this.keyboard(e))
55 document.addEventListener("keyup", e => this.keyboard(e))
56 window.addEventListener("blur", e => this.loseFocus(e))
57 }
58
59 keyboard(event) {
60 if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) {
61 // Ignore everything if the user is entering text somewhere
62 return
63 }
64
65 let down = event.type.endsWith("down")
66
67 if (
68 (event.code == "KeyX")
69 || (event.code == "Period")
70 || (event.code == "BracketLeft")
71 || (event.code == "ControlLeft")
72 || (event.key == "[")
73 ) {
74 // Dit
75 if (this.ditDown != down) {
76 this.keyer.Key(0, down)
77 this.ditDown = down
78 }
79 }
80 if (
81 (event.code == "KeyZ")
82 || (event.code == "Slash")
83 || (event.code == "BracketRight")
84 || (event.code == "ControlRight")
85 || (event.key == "]")
86 ) {
87 if (this.dahDown != down) {
88 this.keyer.Key(1, down)
89 this.dahDown = down
90 }
91 }
92 if (
93 (event.code == "KeyC")
94 || (event.code == "Comma")
95 || (event.key == "Enter")
96 || (event.key == "NumpadEnter")
97 ) {
98 if (this.straightDown != down) {
99 this.keyer.Straight(down)
100 this.straightDown = down
101 }
102 }
103 }
104
105 loseFocus(event) {
106 if (this.ditDown) {
107 this.keyer.Key(0, false)
108 this.ditDown = false
109 }
110 if (this.dahDown) {
111 this.keyer.Key(1, false)
112 this.dahDown = false
113 }
114 if (this.straightDown) {
115 this.keyer.key(2, false)
116 this.straightDown = false
117 }
118 }
119}
120
121export class MIDI extends Input{
122 constructor(keyer) {
123 super(keyer)
124 this.ditDuration = 100
125 this.keyerMode = 0
126
127 this.midiAccess = {outputs: []} // stub while we wait for async stuff
128 if (navigator.requestMIDIAccess) {
129 this.midiInit()
130 }
131 }
132
133 async midiInit(access) {
134 this.inputs = []
135 this.midiAccess = await navigator.requestMIDIAccess()
136 this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e))
137 this.midiStateChange()
138 }
139
140 // If you're looking for the thing that sets the tx tone,
141 // that's in outputs.mjs:SetMIDINote
142
143 sendState() {
144 for (let output of this.midiAccess.outputs.values()) {
145 // Turn off keyboard mode
146 output.send([0xB0, 0x00, 0x00])
147
148 // MIDI only supports 7-bit values, so we have to divide dit duration by two
149 output.send([0xB0, 0x01, this.ditDuration/2])
150
151 // Send keyer mode
152 output.send([0xC0, this.keyerMode])
153 }
154
155 }
156
157 SetDitDuration(duration) {
158 this.ditDuration = duration
159 this.sendState()
160 }
161
162 SetKeyerMode(mode) {
163 this.keyerMode = mode
164 this.sendState()
165 }
166
167 midiStateChange(event) {
168 // Go through this.midiAccess.inputs and only listen on new things
169 for (let input of this.midiAccess.inputs.values()) {
170 if (!this.inputs.includes(input)) {
171 input.addEventListener("midimessage", e => this.midiMessage(e))
172 this.inputs.push(input)
173 }
174 }
175
176 // Tell the Vail adapter to disable keyboard events: we can do MIDI!
177 this.sendState()
178 }
179
180 midiMessage(event) {
181 let data = Array.from(event.data)
182
183 let begin
184 let cmd = data[0] >> 4
185 let chan = data[0] & 0xf
186 switch (cmd) {
187 case 9:
188 begin = true
189 break
190 case 8:
191 begin = false
192 break
193 default:
194 return
195 }
196
197 switch (data[1]) {
198 case 0: // Vail Adapter
199 this.keyer.Straight(begin)
200 break
201 case 1: // Vail Adapter
202 case 20: // N6ARA TinyMIDI
203 this.keyer.Key(0, begin)
204 break
205 case 2: // Vail Adapter
206 case 21: // N6ARA TinyMIDI
207 this.keyer.Key(1, begin)
208 break
209 default:
210 return
211 }
212
213
214 }
215}
216
217export class Gamepad extends Input{
218 constructor(keyer) {
219 super(keyer)
220
221 // Set up for gamepad input
222 window.addEventListener("gamepadconnected", e => this.gamepadConnected(e))
223 }
224
225 /**
226 * Gamepads must be polled, usually at 60fps.
227 * This could be really expensive,
228 * especially on devices with a power budget, like phones.
229 * To be considerate, we only start polling if a gamepad appears.
230 *
231 * @param event Gamepad Connected event
232 */
233 gamepadConnected(event) {
234 if (!this.gamepadButtons) {
235 this.gamepadButtons = {}
236 this.gamepadPoll(event.timeStamp)
237 }
238 }
239
240 gamepadPoll(timestamp) {
241 let currentButtons = {}
242 for (let gp of navigator.getGamepads()) {
243 if (gp == null) {
244 continue
245 }
246 for (let i in gp.buttons) {
247 let pressed = gp.buttons[i].pressed
248 if (i < 2) {
249 currentButtons.key |= pressed
250 } else if (i % 2 == 0) {
251 currentButtons.dit |= pressed
252 } else {
253 currentButtons.dah |= pressed
254 }
255 }
256 }
257
258 if (currentButtons.key != this.gamepadButtons.key) {
259 this.keyer.Straight(currentButtons.key)
260 }
261 if (currentButtons.dit != this.gamepadButtons.dit) {
262 this.keyer.Key(0, currentButtons.dit)
263 }
264 if (currentButtons.dah != this.gamepadButtons.dah) {
265 this.keyer.Key(1, currentButtons.dah)
266 }
267 this.gamepadButtons = currentButtons
268
269 requestAnimationFrame(e => this.gamepadPoll(e))
270 }
271}
272
273class Collection {
274 constructor(keyer) {
275 this.html =new HTML(keyer)
276 this.keyboard =new Keyboard(keyer)
277 this.midi =new MIDI(keyer)
278 this.gamepad =new Gamepad(keyer)
279 this.collection = [this.html, this.keyboard, this.midi, this.gamepad]
280 }
281
282 /**
283 * Set duration of all inputs
284 *
285 * @param duration Duration to set
286 */
287 SetDitDuration(duration) {
288 for (let e of this.collection) {
289 e.SetDitDuration(duration)
290 }
291 }
292
293 /**
294 * Set keyer mode of all inputs
295 *
296 * @param mode Keyer mode to set
297 */
298 SetKeyerMode(mode) {
299 for (let e of this.collection) {
300 e.SetKeyerMode(mode)
301 }
302 }
303}
304
305export {Collection}