web-a2e 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- package/wiki/Video-Rendering.md +658 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* gamepad-handler.js - Physical game controller support via Gamepad API
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY_ENABLED = "gamepad-enabled";
|
|
9
|
+
const STORAGE_KEY_DEADZONE = "gamepad-deadzone";
|
|
10
|
+
const DEFAULT_DEADZONE = 0.1;
|
|
11
|
+
|
|
12
|
+
export class GamepadHandler {
|
|
13
|
+
constructor(wasmModule, joystickWindow) {
|
|
14
|
+
this.wasmModule = wasmModule;
|
|
15
|
+
this.joystickWindow = joystickWindow;
|
|
16
|
+
this.enabled = localStorage.getItem(STORAGE_KEY_ENABLED) !== "false";
|
|
17
|
+
this.deadzone = parseFloat(localStorage.getItem(STORAGE_KEY_DEADZONE)) || DEFAULT_DEADZONE;
|
|
18
|
+
this.gamepadIndex = null;
|
|
19
|
+
this.rafId = null;
|
|
20
|
+
|
|
21
|
+
// Track previous button state to detect edges
|
|
22
|
+
this.prevButtons = [false, false];
|
|
23
|
+
|
|
24
|
+
this._onConnected = this._onConnected.bind(this);
|
|
25
|
+
this._onDisconnected = this._onDisconnected.bind(this);
|
|
26
|
+
this._poll = this._poll.bind(this);
|
|
27
|
+
|
|
28
|
+
window.addEventListener("gamepadconnected", this._onConnected);
|
|
29
|
+
window.addEventListener("gamepaddisconnected", this._onDisconnected);
|
|
30
|
+
|
|
31
|
+
// Check if a gamepad is already connected
|
|
32
|
+
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
|
|
33
|
+
for (let i = 0; i < gamepads.length; i++) {
|
|
34
|
+
if (gamepads[i]) {
|
|
35
|
+
this.gamepadIndex = gamepads[i].index;
|
|
36
|
+
this._notifyWindow();
|
|
37
|
+
if (this.enabled) {
|
|
38
|
+
this._startPolling();
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_onConnected(e) {
|
|
46
|
+
this.gamepadIndex = e.gamepad.index;
|
|
47
|
+
this._notifyWindow();
|
|
48
|
+
if (this.enabled) {
|
|
49
|
+
this._startPolling();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_onDisconnected(e) {
|
|
54
|
+
if (this.gamepadIndex === e.gamepad.index) {
|
|
55
|
+
this.gamepadIndex = null;
|
|
56
|
+
this._notifyWindow();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_notifyWindow() {
|
|
61
|
+
if (this.joystickWindow && this.joystickWindow.updateGamepadStatus) {
|
|
62
|
+
const gp = this._getGamepad();
|
|
63
|
+
this.joystickWindow.updateGamepadStatus(gp ? gp.id : null, this.enabled);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_getGamepad() {
|
|
68
|
+
if (this.gamepadIndex === null) return null;
|
|
69
|
+
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
|
|
70
|
+
return gamepads[this.gamepadIndex] || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_applyDeadzone(value) {
|
|
74
|
+
if (Math.abs(value) < this.deadzone) return 0;
|
|
75
|
+
// Rescale so the range beyond deadzone maps to 0..1
|
|
76
|
+
const sign = value > 0 ? 1 : -1;
|
|
77
|
+
return sign * (Math.abs(value) - this.deadzone) / (1 - this.deadzone);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_poll() {
|
|
81
|
+
const gp = this._getGamepad();
|
|
82
|
+
if (!gp) {
|
|
83
|
+
this.rafId = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.enabled) {
|
|
87
|
+
// Left stick axes (0 = X, 1 = Y), range -1..1
|
|
88
|
+
const rawX = gp.axes[0] || 0;
|
|
89
|
+
const rawY = gp.axes[1] || 0;
|
|
90
|
+
const adjX = this._applyDeadzone(rawX);
|
|
91
|
+
const adjY = this._applyDeadzone(rawY);
|
|
92
|
+
|
|
93
|
+
// Map -1..1 to 0..1
|
|
94
|
+
const normX = (adjX + 1) / 2;
|
|
95
|
+
const normY = (adjY + 1) / 2;
|
|
96
|
+
|
|
97
|
+
// Map to 0-255 for paddle values
|
|
98
|
+
const paddleX = Math.round(normX * 255);
|
|
99
|
+
const paddleY = Math.round(normY * 255);
|
|
100
|
+
|
|
101
|
+
if (this.wasmModule._setPaddleValue) {
|
|
102
|
+
this.wasmModule._setPaddleValue(0, paddleX);
|
|
103
|
+
this.wasmModule._setPaddleValue(1, paddleY);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update the joystick window knob to reflect controller position
|
|
107
|
+
if (this.joystickWindow) {
|
|
108
|
+
this.joystickWindow.setExternalPosition(normX, normY);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Buttons 0 and 1 (A/B on standard controllers)
|
|
112
|
+
for (let i = 0; i < 2; i++) {
|
|
113
|
+
const pressed = gp.buttons[i] ? gp.buttons[i].pressed : false;
|
|
114
|
+
if (pressed !== this.prevButtons[i]) {
|
|
115
|
+
this.prevButtons[i] = pressed;
|
|
116
|
+
if (this.wasmModule._setButton) {
|
|
117
|
+
this.wasmModule._setButton(i, pressed);
|
|
118
|
+
}
|
|
119
|
+
if (this.joystickWindow) {
|
|
120
|
+
this.joystickWindow.setExternalButton(i, pressed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.rafId = requestAnimationFrame(this._poll);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_startPolling() {
|
|
130
|
+
if (this.rafId !== null) return;
|
|
131
|
+
this.rafId = requestAnimationFrame(this._poll);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_stopPolling() {
|
|
135
|
+
if (this.rafId !== null) {
|
|
136
|
+
cancelAnimationFrame(this.rafId);
|
|
137
|
+
this.rafId = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
start() {
|
|
142
|
+
if (this.enabled && this.gamepadIndex !== null) {
|
|
143
|
+
this._startPolling();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stop() {
|
|
148
|
+
this._stopPolling();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setEnabled(enabled) {
|
|
152
|
+
this.enabled = enabled;
|
|
153
|
+
localStorage.setItem(STORAGE_KEY_ENABLED, enabled);
|
|
154
|
+
this._notifyWindow();
|
|
155
|
+
if (enabled && this.gamepadIndex !== null) {
|
|
156
|
+
this._startPolling();
|
|
157
|
+
} else if (!enabled) {
|
|
158
|
+
this._stopPolling();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setDeadzone(value) {
|
|
163
|
+
this.deadzone = Math.max(0, Math.min(0.5, value));
|
|
164
|
+
localStorage.setItem(STORAGE_KEY_DEADZONE, this.deadzone);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
isConnected() {
|
|
168
|
+
return this._getGamepad() !== null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
destroy() {
|
|
172
|
+
this.stop();
|
|
173
|
+
window.removeEventListener("gamepadconnected", this._onConnected);
|
|
174
|
+
window.removeEventListener("gamepaddisconnected", this._onDisconnected);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* index.js - Input subsystem initialization and exports
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { InputHandler } from "./input-handler.js";
|
|
9
|
+
export { TextSelection } from "./text-selection.js";
|
|
10
|
+
export { JoystickWindow } from "./joystick-window.js";
|
|
11
|
+
export { MouseHandler } from "./mouse-handler.js";
|
|
12
|
+
export { GamepadHandler } from "./gamepad-handler.js";
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* input-handler.js - Keyboard input handling for the emulator
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class InputHandler {
|
|
9
|
+
constructor(wasmModule) {
|
|
10
|
+
this.wasmModule = wasmModule;
|
|
11
|
+
|
|
12
|
+
// Canvas element for focus management
|
|
13
|
+
this.canvas = null;
|
|
14
|
+
|
|
15
|
+
// Hidden input for mobile keyboard
|
|
16
|
+
this.mobileInput = null;
|
|
17
|
+
this.isMobile = false;
|
|
18
|
+
|
|
19
|
+
// Paste queue for typing pasted text
|
|
20
|
+
this.pasteQueue = [];
|
|
21
|
+
this.pasteTimer = null;
|
|
22
|
+
this.pasteSpeedUp = false; // whether we've set a speed multiplier for paste
|
|
23
|
+
this.savedSpeedMultiplier = 1; // speed before paste started
|
|
24
|
+
|
|
25
|
+
// MessageChannel for zero-delay batch scheduling (avoids setTimeout's ~4ms minimum)
|
|
26
|
+
this.pasteChannel = new MessageChannel();
|
|
27
|
+
this.pasteChannel.port1.onmessage = () => this.processPasteQueue();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
init() {
|
|
31
|
+
// Detect mobile/touch devices
|
|
32
|
+
this.isMobile = this.detectMobile();
|
|
33
|
+
|
|
34
|
+
// Get canvas and make it focusable
|
|
35
|
+
this.canvas = document.getElementById("screen");
|
|
36
|
+
this.canvas.tabIndex = 1; // Make canvas focusable
|
|
37
|
+
|
|
38
|
+
// Create hidden input for mobile keyboard
|
|
39
|
+
if (this.isMobile) {
|
|
40
|
+
this.createMobileInput();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Focus canvas on click (or mobile input on mobile)
|
|
44
|
+
this.canvas.addEventListener("click", () => {
|
|
45
|
+
if (this.isMobile && this.mobileInput) {
|
|
46
|
+
this.mobileInput.focus();
|
|
47
|
+
} else {
|
|
48
|
+
this.canvas.focus();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Also handle touch events for mobile
|
|
53
|
+
this.canvas.addEventListener("touchend", (e) => {
|
|
54
|
+
if (this.isMobile && this.mobileInput) {
|
|
55
|
+
// Small delay to ensure touch event completes
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
this.mobileInput.focus();
|
|
58
|
+
}, 50);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Focus canvas initially (not on mobile - wait for user tap)
|
|
63
|
+
if (!this.isMobile) {
|
|
64
|
+
setTimeout(() => this.canvas.focus(), 100);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Keyboard event listeners - attach to canvas for better focus control
|
|
68
|
+
this.canvas.addEventListener("keydown", (e) => this.handleKeyDown(e));
|
|
69
|
+
this.canvas.addEventListener("keyup", (e) => this.handleKeyUp(e));
|
|
70
|
+
|
|
71
|
+
// Also listen on document but only process if canvas has focus or no other element does
|
|
72
|
+
document.addEventListener("keydown", (e) => {
|
|
73
|
+
// Only handle if canvas has focus or the active element is body
|
|
74
|
+
if (
|
|
75
|
+
document.activeElement === this.canvas ||
|
|
76
|
+
document.activeElement === document.body
|
|
77
|
+
) {
|
|
78
|
+
this.handleKeyDown(e);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
document.addEventListener("keyup", (e) => {
|
|
83
|
+
if (
|
|
84
|
+
document.activeElement === this.canvas ||
|
|
85
|
+
document.activeElement === document.body
|
|
86
|
+
) {
|
|
87
|
+
this.handleKeyUp(e);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Paste event listener
|
|
92
|
+
document.addEventListener("paste", (e) => {
|
|
93
|
+
if (
|
|
94
|
+
document.activeElement === this.canvas ||
|
|
95
|
+
document.activeElement === document.body
|
|
96
|
+
) {
|
|
97
|
+
this.handlePaste(e);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
handleKeyDown(event) {
|
|
103
|
+
const keyCode = event.keyCode || event.which;
|
|
104
|
+
|
|
105
|
+
// Get modifier states
|
|
106
|
+
const shift = event.shiftKey;
|
|
107
|
+
const ctrl = event.ctrlKey;
|
|
108
|
+
const alt = event.altKey;
|
|
109
|
+
const meta = event.metaKey;
|
|
110
|
+
const capsLock = event.getModifierState && event.getModifierState('CapsLock');
|
|
111
|
+
|
|
112
|
+
// Don't interfere with browser shortcuts
|
|
113
|
+
if (ctrl && keyCode === 82) {
|
|
114
|
+
// Ctrl+R for refresh
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Prevent default for these keys when not using modifiers
|
|
119
|
+
if (this.shouldPreventDefault(event)) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Always prevent default for printable characters when canvas has focus
|
|
124
|
+
// to stop them from triggering button shortcuts
|
|
125
|
+
if (document.activeElement === this.canvas) {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Send raw keycode to WASM - C++ handles the translation
|
|
130
|
+
this.wasmModule._handleRawKeyDown(keyCode, shift, ctrl, alt, meta, capsLock);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleKeyUp(event) {
|
|
134
|
+
const keyCode = event.keyCode || event.which;
|
|
135
|
+
|
|
136
|
+
// Get modifier states
|
|
137
|
+
const shift = event.shiftKey;
|
|
138
|
+
const ctrl = event.ctrlKey;
|
|
139
|
+
const alt = event.altKey;
|
|
140
|
+
const meta = event.metaKey;
|
|
141
|
+
|
|
142
|
+
// Send raw keycode to WASM
|
|
143
|
+
this.wasmModule._handleRawKeyUp(keyCode, shift, ctrl, alt, meta);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
shouldPreventDefault(event) {
|
|
147
|
+
const keyCode = event.keyCode || event.which;
|
|
148
|
+
|
|
149
|
+
// Prevent default for these keys when not using modifiers
|
|
150
|
+
const preventKeys = [
|
|
151
|
+
8, // Backspace
|
|
152
|
+
9, // Tab
|
|
153
|
+
27, // Escape
|
|
154
|
+
32, // Space (prevent page scroll)
|
|
155
|
+
37, 38, 39, 40, // Arrow keys
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
if (preventKeys.includes(keyCode) && !event.ctrlKey && !event.metaKey) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle paste event - queues text for input at accelerated speed
|
|
166
|
+
handlePaste(event) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
|
|
169
|
+
const text = (event.clipboardData || window.clipboardData).getData("text");
|
|
170
|
+
if (!text) return;
|
|
171
|
+
|
|
172
|
+
this.queueTextInput(text, { speedMultiplier: 8 });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Convert character to Apple II key code (for paste only)
|
|
176
|
+
charToAppleKey(char) {
|
|
177
|
+
const result = this.wasmModule._charToAppleKey(char.charCodeAt(0));
|
|
178
|
+
return result >= 0 ? result : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Process paste queue in batches, yielding to the browser periodically
|
|
182
|
+
// to keep the UI responsive and let the audio worklet drive emulation.
|
|
183
|
+
// Speed multiplier (set via WASM) makes the audio-driven emulation run
|
|
184
|
+
// faster, while small boost cycle batches handle immediate key processing.
|
|
185
|
+
processPasteQueue() {
|
|
186
|
+
if (this.pasteQueue.length === 0) {
|
|
187
|
+
this.restorePasteSpeed();
|
|
188
|
+
this.pasteTimer = null;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const BOOST_BATCH = 500; // small cycle batch for immediate key processing
|
|
193
|
+
const TIME_BUDGET_MS = 30;
|
|
194
|
+
const batchEnd = performance.now() + TIME_BUDGET_MS;
|
|
195
|
+
|
|
196
|
+
while (this.pasteQueue.length > 0 && performance.now() < batchEnd) {
|
|
197
|
+
// Wait for keyboard ready, running small boost cycles until it is
|
|
198
|
+
if (this.wasmModule._isKeyboardReady) {
|
|
199
|
+
while (!this.wasmModule._isKeyboardReady()) {
|
|
200
|
+
this.wasmModule._runCycles(BOOST_BATCH);
|
|
201
|
+
if (performance.now() >= batchEnd) break;
|
|
202
|
+
}
|
|
203
|
+
if (!this.wasmModule._isKeyboardReady()) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const appleKey = this.pasteQueue.shift();
|
|
209
|
+
this.wasmModule._keyDown(appleKey);
|
|
210
|
+
|
|
211
|
+
// Run a small burst to start processing the keystroke
|
|
212
|
+
this.wasmModule._runCycles(BOOST_BATCH);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Schedule next batch if more characters remain
|
|
216
|
+
if (this.pasteQueue.length > 0) {
|
|
217
|
+
this.pasteTimer = true;
|
|
218
|
+
this.pasteChannel.port2.postMessage(null);
|
|
219
|
+
} else {
|
|
220
|
+
this.restorePasteSpeed();
|
|
221
|
+
this.pasteTimer = null;
|
|
222
|
+
if (this.pasteOnComplete) {
|
|
223
|
+
this.pasteOnComplete(false); // false = completed normally
|
|
224
|
+
this.pasteOnComplete = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Set emulation speed multiplier for fast paste/input
|
|
230
|
+
setPasteSpeed(multiplier) {
|
|
231
|
+
if (!this.pasteSpeedUp && this.wasmModule._setSpeedMultiplier) {
|
|
232
|
+
this.savedSpeedMultiplier = this.wasmModule._getSpeedMultiplier
|
|
233
|
+
? this.wasmModule._getSpeedMultiplier()
|
|
234
|
+
: 1;
|
|
235
|
+
this.wasmModule._setSpeedMultiplier(multiplier);
|
|
236
|
+
this.pasteSpeedUp = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Restore emulation speed after paste completes
|
|
241
|
+
restorePasteSpeed() {
|
|
242
|
+
if (this.pasteSpeedUp && this.wasmModule._setSpeedMultiplier) {
|
|
243
|
+
this.wasmModule._setSpeedMultiplier(this.savedSpeedMultiplier);
|
|
244
|
+
this.pasteSpeedUp = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Cancel any pending paste operation
|
|
249
|
+
cancelPaste() {
|
|
250
|
+
this.pasteTimer = null;
|
|
251
|
+
this.pasteQueue = [];
|
|
252
|
+
this.restorePasteSpeed();
|
|
253
|
+
if (this.pasteOnComplete) {
|
|
254
|
+
this.pasteOnComplete(true); // true = cancelled
|
|
255
|
+
this.pasteOnComplete = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Queue text for programmatic input (used by BasicProgramWindow)
|
|
260
|
+
// speedMultiplier: emulation speed during input (1=normal, 8=8x)
|
|
261
|
+
// onStart: callback when pasting begins
|
|
262
|
+
// onComplete: callback when pasting finishes (or is cancelled)
|
|
263
|
+
queueTextInput(text, { speedMultiplier = 8, onStart = null, onComplete = null } = {}) {
|
|
264
|
+
this.pasteOnComplete = onComplete;
|
|
265
|
+
this.setPasteSpeed(speedMultiplier);
|
|
266
|
+
|
|
267
|
+
for (const char of text) {
|
|
268
|
+
const appleKey = this.charToAppleKey(char);
|
|
269
|
+
if (appleKey !== null) {
|
|
270
|
+
this.pasteQueue.push(appleKey);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Start processing queue if not already running
|
|
275
|
+
if (!this.pasteTimer && this.pasteQueue.length > 0) {
|
|
276
|
+
if (onStart) onStart();
|
|
277
|
+
this.processPasteQueue();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check if a paste operation is in progress
|
|
282
|
+
isPasting() {
|
|
283
|
+
return this.pasteQueue.length > 0 || this.pasteTimer !== null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Detect if we're on a mobile/touch device
|
|
287
|
+
detectMobile() {
|
|
288
|
+
// Check for touch capability and mobile user agent
|
|
289
|
+
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
290
|
+
const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
291
|
+
// Also check for small screen width as a fallback
|
|
292
|
+
const isSmallScreen = window.innerWidth <= 800;
|
|
293
|
+
|
|
294
|
+
return hasTouch && (isMobileUA || isSmallScreen);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Create hidden input element for mobile keyboard
|
|
298
|
+
createMobileInput() {
|
|
299
|
+
this.mobileInput = document.createElement('input');
|
|
300
|
+
this.mobileInput.type = 'text';
|
|
301
|
+
this.mobileInput.id = 'mobile-keyboard-input';
|
|
302
|
+
this.mobileInput.autocomplete = 'off';
|
|
303
|
+
this.mobileInput.autocapitalize = 'none';
|
|
304
|
+
this.mobileInput.autocorrect = 'off';
|
|
305
|
+
this.mobileInput.spellcheck = false;
|
|
306
|
+
|
|
307
|
+
// Style to be invisible but still functional
|
|
308
|
+
Object.assign(this.mobileInput.style, {
|
|
309
|
+
position: 'absolute',
|
|
310
|
+
left: '-9999px',
|
|
311
|
+
top: '0',
|
|
312
|
+
width: '1px',
|
|
313
|
+
height: '1px',
|
|
314
|
+
opacity: '0',
|
|
315
|
+
pointerEvents: 'none',
|
|
316
|
+
zIndex: '-1'
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
document.body.appendChild(this.mobileInput);
|
|
320
|
+
|
|
321
|
+
// Handle input events from mobile keyboard
|
|
322
|
+
this.mobileInput.addEventListener('input', (e) => {
|
|
323
|
+
const data = e.data;
|
|
324
|
+
if (data) {
|
|
325
|
+
// Process each character typed
|
|
326
|
+
for (const char of data) {
|
|
327
|
+
this.sendCharToEmulator(char);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Clear the input to be ready for next character
|
|
331
|
+
this.mobileInput.value = '';
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Handle special keys via keydown
|
|
335
|
+
this.mobileInput.addEventListener('keydown', (e) => {
|
|
336
|
+
const keyCode = e.keyCode || e.which;
|
|
337
|
+
|
|
338
|
+
// Handle special keys that don't generate input events
|
|
339
|
+
switch (keyCode) {
|
|
340
|
+
case 8: // Backspace
|
|
341
|
+
case 13: // Enter
|
|
342
|
+
case 27: // Escape
|
|
343
|
+
case 9: // Tab
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
this.handleKeyDown(e);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.mobileInput.addEventListener('keyup', (e) => {
|
|
351
|
+
const keyCode = e.keyCode || e.which;
|
|
352
|
+
|
|
353
|
+
// Handle special key releases
|
|
354
|
+
switch (keyCode) {
|
|
355
|
+
case 8: // Backspace
|
|
356
|
+
case 13: // Enter
|
|
357
|
+
case 27: // Escape
|
|
358
|
+
case 9: // Tab
|
|
359
|
+
this.handleKeyUp(e);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Handle blur - show visual feedback that keyboard is hidden
|
|
365
|
+
this.mobileInput.addEventListener('blur', () => {
|
|
366
|
+
this.canvas.classList.remove('keyboard-active');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Handle focus - show visual feedback that keyboard is active
|
|
370
|
+
this.mobileInput.addEventListener('focus', () => {
|
|
371
|
+
this.canvas.classList.add('keyboard-active');
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Send a character to the emulator (for mobile input)
|
|
376
|
+
sendCharToEmulator(char) {
|
|
377
|
+
const appleKey = this.charToAppleKey(char);
|
|
378
|
+
if (appleKey !== null) {
|
|
379
|
+
this.wasmModule._keyDown(appleKey);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Show mobile keyboard programmatically
|
|
384
|
+
showMobileKeyboard() {
|
|
385
|
+
if (this.isMobile && this.mobileInput) {
|
|
386
|
+
this.mobileInput.focus();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Hide mobile keyboard
|
|
391
|
+
hideMobileKeyboard() {
|
|
392
|
+
if (this.isMobile && this.mobileInput) {
|
|
393
|
+
this.mobileInput.blur();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|