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,301 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* disk-persistence.js - Disk image persistence to IndexedDB
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createDatabaseManager } from "../utils/indexeddb-helper.js";
|
|
9
|
+
|
|
10
|
+
const DB_NAME = "a2e-disk-persistence";
|
|
11
|
+
const DB_VERSION = 3;
|
|
12
|
+
const STORE_NAME = "disks";
|
|
13
|
+
const RECENT_STORE_NAME = "recentDisks";
|
|
14
|
+
const MAX_RECENT_DISKS = 10;
|
|
15
|
+
|
|
16
|
+
const db = createDatabaseManager({
|
|
17
|
+
dbName: DB_NAME,
|
|
18
|
+
version: DB_VERSION,
|
|
19
|
+
onUpgrade: (event) => {
|
|
20
|
+
const database = event.target.result;
|
|
21
|
+
const oldVersion = event.oldVersion;
|
|
22
|
+
|
|
23
|
+
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
|
24
|
+
database.createObjectStore(STORE_NAME, { keyPath: "driveNum" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// v2: Add recent disks store
|
|
28
|
+
if (!database.objectStoreNames.contains(RECENT_STORE_NAME)) {
|
|
29
|
+
const recentStore = database.createObjectStore(RECENT_STORE_NAME, {
|
|
30
|
+
keyPath: "id",
|
|
31
|
+
autoIncrement: true,
|
|
32
|
+
});
|
|
33
|
+
recentStore.createIndex("filename", "filename", { unique: false });
|
|
34
|
+
recentStore.createIndex("accessedAt", "accessedAt", { unique: false });
|
|
35
|
+
recentStore.createIndex("driveNum", "driveNum", { unique: false });
|
|
36
|
+
} else if (oldVersion < 3) {
|
|
37
|
+
// v3: Add driveNum index to existing store
|
|
38
|
+
const transaction = event.target.transaction;
|
|
39
|
+
const recentStore = transaction.objectStore(RECENT_STORE_NAME);
|
|
40
|
+
if (!recentStore.indexNames.contains("driveNum")) {
|
|
41
|
+
recentStore.createIndex("driveNum", "driveNum", { unique: false });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save a disk image to IndexedDB
|
|
49
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
50
|
+
* @param {string} filename - The disk filename
|
|
51
|
+
* @param {Uint8Array} data - The disk image data
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
export async function saveDiskToStorage(driveNum, filename, data) {
|
|
55
|
+
try {
|
|
56
|
+
const diskRecord = {
|
|
57
|
+
driveNum,
|
|
58
|
+
filename,
|
|
59
|
+
data: new Uint8Array(data),
|
|
60
|
+
savedAt: Date.now(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await db.put(STORE_NAME, diskRecord);
|
|
64
|
+
console.log(`Saved disk to storage: drive ${driveNum + 1}, ${filename}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Error saving disk to storage:", error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load a disk image from IndexedDB
|
|
72
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
73
|
+
* @returns {Promise<{filename: string, data: Uint8Array} | null>}
|
|
74
|
+
*/
|
|
75
|
+
export async function loadDiskFromStorage(driveNum) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await db.get(STORE_NAME, driveNum);
|
|
78
|
+
if (result) {
|
|
79
|
+
console.log(`Loaded disk from storage: drive ${driveNum + 1}, ${result.filename}`);
|
|
80
|
+
return {
|
|
81
|
+
filename: result.filename,
|
|
82
|
+
data: new Uint8Array(result.data),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("Error loading disk from storage:", error);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove a disk from IndexedDB storage
|
|
94
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
95
|
+
* @returns {Promise<void>}
|
|
96
|
+
*/
|
|
97
|
+
export async function clearDiskFromStorage(driveNum) {
|
|
98
|
+
try {
|
|
99
|
+
await db.remove(STORE_NAME, driveNum);
|
|
100
|
+
console.log(`Cleared disk from storage: drive ${driveNum + 1}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error clearing disk from storage:", error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if there are any persisted disks
|
|
108
|
+
* @returns {Promise<boolean>}
|
|
109
|
+
*/
|
|
110
|
+
export async function hasPersistedDisks() {
|
|
111
|
+
try {
|
|
112
|
+
const count = await db.count(STORE_NAME);
|
|
113
|
+
return count > 0;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Error checking persisted disks:", error);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Recent Disks Functions
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find a recent disk by filename for a specific drive
|
|
126
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
127
|
+
* @param {string} filename
|
|
128
|
+
* @returns {Promise<number|null>} The record ID or null
|
|
129
|
+
*/
|
|
130
|
+
async function findRecentDiskByFilename(driveNum, filename) {
|
|
131
|
+
let foundId = null;
|
|
132
|
+
|
|
133
|
+
await db.iterate(
|
|
134
|
+
RECENT_STORE_NAME,
|
|
135
|
+
{ indexName: "filename", range: IDBKeyRange.only(filename) },
|
|
136
|
+
(value, cursor) => {
|
|
137
|
+
if (value.driveNum === driveNum) {
|
|
138
|
+
foundId = cursor.primaryKey;
|
|
139
|
+
return false; // Stop iteration
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return foundId;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Trim recent disks to MAX_RECENT_DISKS entries for a specific drive
|
|
149
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async function trimRecentDisks(driveNum) {
|
|
153
|
+
// Collect all records for this drive
|
|
154
|
+
const records = [];
|
|
155
|
+
|
|
156
|
+
await db.iterate(
|
|
157
|
+
RECENT_STORE_NAME,
|
|
158
|
+
{ indexName: "accessedAt" },
|
|
159
|
+
(value, cursor) => {
|
|
160
|
+
if (value.driveNum === driveNum) {
|
|
161
|
+
records.push({ id: cursor.primaryKey, accessedAt: value.accessedAt });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Delete excess entries (oldest first since sorted by accessedAt)
|
|
167
|
+
if (records.length > MAX_RECENT_DISKS) {
|
|
168
|
+
const deleteCount = records.length - MAX_RECENT_DISKS;
|
|
169
|
+
for (let i = 0; i < deleteCount; i++) {
|
|
170
|
+
await db.remove(RECENT_STORE_NAME, records[i].id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Add a disk to the recent disks list for a specific drive
|
|
177
|
+
* If the disk already exists (same filename for same drive), update its access time
|
|
178
|
+
* Maintains a maximum of MAX_RECENT_DISKS entries per drive
|
|
179
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
180
|
+
* @param {string} filename - The disk filename
|
|
181
|
+
* @param {Uint8Array} data - The disk image data
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
export async function addToRecentDisks(driveNum, filename, data) {
|
|
185
|
+
try {
|
|
186
|
+
// Check if this filename already exists for this drive and remove it
|
|
187
|
+
const existingId = await findRecentDiskByFilename(driveNum, filename);
|
|
188
|
+
if (existingId !== null) {
|
|
189
|
+
await db.remove(RECENT_STORE_NAME, existingId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add the new entry
|
|
193
|
+
const diskRecord = {
|
|
194
|
+
driveNum,
|
|
195
|
+
filename,
|
|
196
|
+
data: new Uint8Array(data),
|
|
197
|
+
accessedAt: Date.now(),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await db.add(RECENT_STORE_NAME, diskRecord);
|
|
201
|
+
|
|
202
|
+
// Trim to MAX_RECENT_DISKS for this drive
|
|
203
|
+
await trimRecentDisks(driveNum);
|
|
204
|
+
|
|
205
|
+
console.log(`Added to recent disks (drive ${driveNum + 1}): ${filename}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error("Error adding to recent disks:", error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the list of recent disks for a specific drive, sorted by most recently accessed
|
|
213
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
214
|
+
* @returns {Promise<Array<{id: number, filename: string, accessedAt: number}>>}
|
|
215
|
+
*/
|
|
216
|
+
export async function getRecentDisks(driveNum) {
|
|
217
|
+
try {
|
|
218
|
+
const results = [];
|
|
219
|
+
|
|
220
|
+
await db.iterate(
|
|
221
|
+
RECENT_STORE_NAME,
|
|
222
|
+
{ indexName: "accessedAt", direction: "prev" },
|
|
223
|
+
(value) => {
|
|
224
|
+
if (value.driveNum === driveNum) {
|
|
225
|
+
results.push({
|
|
226
|
+
id: value.id,
|
|
227
|
+
filename: value.filename,
|
|
228
|
+
accessedAt: value.accessedAt,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return results;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("Error getting recent disks:", error);
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Load a recent disk by its ID
|
|
243
|
+
* @param {number} id - The record ID
|
|
244
|
+
* @returns {Promise<{filename: string, data: Uint8Array} | null>}
|
|
245
|
+
*/
|
|
246
|
+
export async function loadRecentDisk(id) {
|
|
247
|
+
try {
|
|
248
|
+
const result = await db.get(RECENT_STORE_NAME, id);
|
|
249
|
+
if (result) {
|
|
250
|
+
return {
|
|
251
|
+
filename: result.filename,
|
|
252
|
+
data: new Uint8Array(result.data),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error("Error loading recent disk:", error);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Remove a disk from the recent list
|
|
264
|
+
* @param {number} id - The record ID
|
|
265
|
+
* @returns {Promise<void>}
|
|
266
|
+
*/
|
|
267
|
+
export async function removeRecentDisk(id) {
|
|
268
|
+
try {
|
|
269
|
+
await db.remove(RECENT_STORE_NAME, id);
|
|
270
|
+
console.log(`Removed recent disk with id: ${id}`);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error("Error removing recent disk:", error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Clear all recent disks for a specific drive
|
|
278
|
+
* @param {number} driveNum - Drive number (0 or 1)
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
export async function clearRecentDisks(driveNum) {
|
|
282
|
+
try {
|
|
283
|
+
// Collect IDs to delete
|
|
284
|
+
const idsToDelete = [];
|
|
285
|
+
|
|
286
|
+
await db.iterate(RECENT_STORE_NAME, {}, (value, cursor) => {
|
|
287
|
+
if (value.driveNum === driveNum) {
|
|
288
|
+
idsToDelete.push(cursor.primaryKey);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Delete them
|
|
293
|
+
for (const id of idsToDelete) {
|
|
294
|
+
await db.remove(RECENT_STORE_NAME, id);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log(`Cleared recent disks for drive ${driveNum + 1}`);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error("Error clearing recent disks:", error);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* disk-surface-renderer.js - Disk surface visualization renderer
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CANVAS_W = 280;
|
|
9
|
+
const CANVAS_H = 240;
|
|
10
|
+
const CENTER_X = 140;
|
|
11
|
+
const CENTER_Y = 115;
|
|
12
|
+
const OUTER_RADIUS = 105;
|
|
13
|
+
const HUB_HOLE_RADIUS = 28; // large center hole (~27% of disk)
|
|
14
|
+
const HUB_RING_INNER = 28; // white reinforcement ring starts at hole edge
|
|
15
|
+
const HUB_RING_OUTER = 34; // ring is ~6px wide
|
|
16
|
+
const TRACK_OUTER = OUTER_RADIUS - 3; // outermost track position
|
|
17
|
+
const TRACK_INNER = HUB_RING_OUTER + 4; // innermost track position (just outside hub ring)
|
|
18
|
+
const NUM_TRACKS = 35;
|
|
19
|
+
const NUM_SECTORS = 16;
|
|
20
|
+
const TRACK_RANGE = TRACK_OUTER - TRACK_INNER;
|
|
21
|
+
const INDEX_HOLE_RADIUS = 4;
|
|
22
|
+
const INDEX_HOLE_DIST = (HUB_RING_INNER + HUB_RING_OUTER) / 2; // centered in hub ring
|
|
23
|
+
const RPM_RAD_PER_MS = Math.PI / 100; // 300 RPM
|
|
24
|
+
const PX_RATIO = 2; // backing store scale for sharper rendering
|
|
25
|
+
|
|
26
|
+
export class DiskSurfaceRenderer {
|
|
27
|
+
constructor(canvas) {
|
|
28
|
+
this.canvas = canvas;
|
|
29
|
+
|
|
30
|
+
// Set high-res backing store; CSS sizes the element
|
|
31
|
+
canvas.width = CANVAS_W * PX_RATIO;
|
|
32
|
+
canvas.height = CANVAS_H * PX_RATIO;
|
|
33
|
+
|
|
34
|
+
this.ctx = canvas.getContext('2d');
|
|
35
|
+
this.ctx.scale(PX_RATIO, PX_RATIO);
|
|
36
|
+
|
|
37
|
+
// Rotation state
|
|
38
|
+
this.angle = 0;
|
|
39
|
+
this.lastTimestamp = 0;
|
|
40
|
+
this.motorOn = false;
|
|
41
|
+
this.angularVelocity = 0;
|
|
42
|
+
this.spinning = false;
|
|
43
|
+
|
|
44
|
+
// Previous state for dirty checking
|
|
45
|
+
this._prev = {};
|
|
46
|
+
|
|
47
|
+
// Theme colors (read from CSS variables)
|
|
48
|
+
this._lastTheme = null;
|
|
49
|
+
this._updateThemeColors();
|
|
50
|
+
|
|
51
|
+
this._drawEmpty();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_updateThemeColors() {
|
|
55
|
+
const s = getComputedStyle(document.documentElement);
|
|
56
|
+
const v = (name, fallback) => s.getPropertyValue(name).trim() || fallback;
|
|
57
|
+
this._colors = {
|
|
58
|
+
bg: v('--disk-bg', '#161b22'),
|
|
59
|
+
medium: v('--disk-medium', '#1a1308'),
|
|
60
|
+
sectorLine: v('--disk-sector-line', 'rgba(255,255,255,0.08)'),
|
|
61
|
+
ghostOutline: v('--disk-ghost-outline', 'rgba(255,255,255,0.06)'),
|
|
62
|
+
ghostSector: v('--disk-ghost-sector', 'rgba(255,255,255,0.03)'),
|
|
63
|
+
ghostHub: v('--disk-ghost-hub', 'rgba(210,208,200,0.08)'),
|
|
64
|
+
hubRing: v('--disk-hub-ring', 'rgba(210,208,200,0.85)'),
|
|
65
|
+
hubEdge: v('--disk-hub-edge', 'rgba(255,255,255,0.12)'),
|
|
66
|
+
hubEdgeInner: v('--disk-hub-edge-inner', 'rgba(255,255,255,0.08)'),
|
|
67
|
+
holeEdge: v('--disk-hole-edge', 'rgba(0,0,0,0.3)'),
|
|
68
|
+
diskEdge: v('--disk-edge', 'rgba(255,255,255,0.06)'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
update(state) {
|
|
73
|
+
const {
|
|
74
|
+
hasDisk, isActive, isWriteMode, quarterTrack, track,
|
|
75
|
+
trackAccessCounts, maxAccessCount, diskColor, timestamp
|
|
76
|
+
} = state;
|
|
77
|
+
|
|
78
|
+
// Check for theme changes
|
|
79
|
+
const currentTheme = document.documentElement.dataset.theme;
|
|
80
|
+
if (this._lastTheme !== currentTheme) {
|
|
81
|
+
this._lastTheme = currentTheme;
|
|
82
|
+
this._updateThemeColors();
|
|
83
|
+
this._prev = {}; // force full redraw
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Rotation physics
|
|
87
|
+
const dt = this.lastTimestamp > 0 ? timestamp - this.lastTimestamp : 0;
|
|
88
|
+
this.lastTimestamp = timestamp;
|
|
89
|
+
|
|
90
|
+
if (isActive && hasDisk) {
|
|
91
|
+
this.motorOn = true;
|
|
92
|
+
this.spinning = true;
|
|
93
|
+
this.angularVelocity = RPM_RAD_PER_MS;
|
|
94
|
+
} else if (this.motorOn) {
|
|
95
|
+
this.motorOn = false;
|
|
96
|
+
this._prev = {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.spinning && dt > 0) {
|
|
100
|
+
if (!this.motorOn) {
|
|
101
|
+
this.angularVelocity *= Math.pow(0.5, dt / 600);
|
|
102
|
+
if (this.angularVelocity < RPM_RAD_PER_MS * 0.005) {
|
|
103
|
+
this.angularVelocity = 0;
|
|
104
|
+
this.spinning = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.angle += this.angularVelocity * dt;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!hasDisk) {
|
|
111
|
+
if (this._prev.hasDisk !== false) {
|
|
112
|
+
this._drawEmpty();
|
|
113
|
+
this._prev = { hasDisk: false };
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!this.spinning) {
|
|
119
|
+
if (
|
|
120
|
+
this._prev.hasDisk === true &&
|
|
121
|
+
this._prev.quarterTrack === quarterTrack &&
|
|
122
|
+
this._prev.track === track &&
|
|
123
|
+
this._prev.isActive === isActive &&
|
|
124
|
+
this._prev.isWriteMode === isWriteMode &&
|
|
125
|
+
this._prev.maxAccessCount === maxAccessCount &&
|
|
126
|
+
this._prev.diskColor === diskColor
|
|
127
|
+
) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this._prev = {
|
|
133
|
+
hasDisk: true, quarterTrack, track, isActive,
|
|
134
|
+
isWriteMode, maxAccessCount, diskColor
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this._drawDisk(state);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
reset() {
|
|
141
|
+
this.angle = 0;
|
|
142
|
+
this.lastTimestamp = 0;
|
|
143
|
+
this.motorOn = false;
|
|
144
|
+
this.angularVelocity = 0;
|
|
145
|
+
this.spinning = false;
|
|
146
|
+
this._prev = {};
|
|
147
|
+
this._drawEmpty();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- Drawing ----
|
|
151
|
+
|
|
152
|
+
_drawEmpty() {
|
|
153
|
+
const ctx = this.ctx;
|
|
154
|
+
const c = this._colors;
|
|
155
|
+
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
|
|
156
|
+
|
|
157
|
+
ctx.fillStyle = c.bg;
|
|
158
|
+
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
|
|
159
|
+
|
|
160
|
+
// Ghost disk — faint outline of the platter
|
|
161
|
+
ctx.beginPath();
|
|
162
|
+
ctx.arc(CENTER_X, CENTER_Y, OUTER_RADIUS, 0, Math.PI * 2);
|
|
163
|
+
ctx.strokeStyle = c.ghostOutline;
|
|
164
|
+
ctx.lineWidth = 1;
|
|
165
|
+
ctx.stroke();
|
|
166
|
+
|
|
167
|
+
// Ghost sector lines
|
|
168
|
+
const TWO_PI = Math.PI * 2;
|
|
169
|
+
ctx.save();
|
|
170
|
+
ctx.translate(CENTER_X, CENTER_Y);
|
|
171
|
+
for (let s = 0; s < NUM_SECTORS; s++) {
|
|
172
|
+
const a = (s / NUM_SECTORS) * TWO_PI;
|
|
173
|
+
ctx.strokeStyle = c.ghostSector;
|
|
174
|
+
ctx.lineWidth = 0.5;
|
|
175
|
+
ctx.beginPath();
|
|
176
|
+
ctx.moveTo(Math.cos(a) * TRACK_INNER, Math.sin(a) * TRACK_INNER);
|
|
177
|
+
ctx.lineTo(Math.cos(a) * TRACK_OUTER, Math.sin(a) * TRACK_OUTER);
|
|
178
|
+
ctx.stroke();
|
|
179
|
+
}
|
|
180
|
+
ctx.restore();
|
|
181
|
+
|
|
182
|
+
// Ghost hub ring
|
|
183
|
+
ctx.beginPath();
|
|
184
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_RING_OUTER, 0, Math.PI * 2);
|
|
185
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_RING_INNER, TWO_PI, 0, true);
|
|
186
|
+
ctx.fillStyle = c.ghostHub;
|
|
187
|
+
ctx.fill();
|
|
188
|
+
|
|
189
|
+
// Ghost center hole
|
|
190
|
+
ctx.beginPath();
|
|
191
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_HOLE_RADIUS, 0, Math.PI * 2);
|
|
192
|
+
ctx.strokeStyle = c.ghostOutline;
|
|
193
|
+
ctx.lineWidth = 0.5;
|
|
194
|
+
ctx.stroke();
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_drawDisk(state) {
|
|
199
|
+
const {
|
|
200
|
+
isActive, isWriteMode, quarterTrack,
|
|
201
|
+
trackAccessCounts, maxAccessCount, diskColor
|
|
202
|
+
} = state;
|
|
203
|
+
|
|
204
|
+
const ctx = this.ctx;
|
|
205
|
+
const c = this._colors;
|
|
206
|
+
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
|
|
207
|
+
|
|
208
|
+
// Background
|
|
209
|
+
ctx.fillStyle = c.bg;
|
|
210
|
+
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
|
|
211
|
+
|
|
212
|
+
// 1. Magnetic medium — dark brown disk
|
|
213
|
+
ctx.beginPath();
|
|
214
|
+
ctx.arc(CENTER_X, CENTER_Y, OUTER_RADIUS, 0, Math.PI * 2);
|
|
215
|
+
ctx.fillStyle = c.medium;
|
|
216
|
+
ctx.fill();
|
|
217
|
+
|
|
218
|
+
// Subtle edge
|
|
219
|
+
ctx.strokeStyle = c.diskEdge;
|
|
220
|
+
ctx.lineWidth = 0.5;
|
|
221
|
+
ctx.stroke();
|
|
222
|
+
|
|
223
|
+
// 2. Accessed track rings only (rotated with disk)
|
|
224
|
+
ctx.save();
|
|
225
|
+
ctx.translate(CENTER_X, CENTER_Y);
|
|
226
|
+
ctx.rotate(this.angle);
|
|
227
|
+
this._drawAccessedTracks(ctx, trackAccessCounts, maxAccessCount);
|
|
228
|
+
|
|
229
|
+
// 3. Sector lines (rotated)
|
|
230
|
+
this._drawSectorLines(ctx);
|
|
231
|
+
ctx.restore();
|
|
232
|
+
|
|
233
|
+
// 5. Head and activity glow
|
|
234
|
+
this._drawHeadArm(ctx, quarterTrack, isActive, isWriteMode);
|
|
235
|
+
|
|
236
|
+
if (isActive) {
|
|
237
|
+
this._drawHeadGlow(ctx, quarterTrack, isWriteMode);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 7. Hub ring + center hole
|
|
241
|
+
this._drawHub(ctx);
|
|
242
|
+
|
|
243
|
+
// 8. Index hole — punched through the hub ring, rotates with disk
|
|
244
|
+
this._drawIndexHole(ctx);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_drawAccessedTracks(ctx, trackAccessCounts, maxAccessCount) {
|
|
248
|
+
if (!trackAccessCounts || maxAccessCount === 0) return;
|
|
249
|
+
|
|
250
|
+
const logMax = Math.log(maxAccessCount + 1);
|
|
251
|
+
|
|
252
|
+
for (let t = 0; t < NUM_TRACKS; t++) {
|
|
253
|
+
const count = trackAccessCounts[t];
|
|
254
|
+
if (count === 0) continue;
|
|
255
|
+
|
|
256
|
+
// Track 0 = outermost, track 34 = innermost
|
|
257
|
+
const outerR = TRACK_OUTER - (t * TRACK_RANGE / NUM_TRACKS);
|
|
258
|
+
const innerR = TRACK_OUTER - ((t + 1) * TRACK_RANGE / NUM_TRACKS);
|
|
259
|
+
|
|
260
|
+
const intensity = Math.log(count + 1) / logMax;
|
|
261
|
+
const r = Math.round(40 + 215 * intensity);
|
|
262
|
+
const g = Math.round(60 + 80 * intensity - 40 * intensity * intensity);
|
|
263
|
+
const b = Math.round(100 - 80 * intensity);
|
|
264
|
+
const a = 0.3 + 0.55 * intensity;
|
|
265
|
+
|
|
266
|
+
ctx.fillStyle = `rgba(${r},${g},${b},${a})`;
|
|
267
|
+
ctx.beginPath();
|
|
268
|
+
ctx.arc(0, 0, outerR - 0.5, 0, Math.PI * 2);
|
|
269
|
+
ctx.arc(0, 0, innerR + 0.5, Math.PI * 2, 0, true);
|
|
270
|
+
ctx.fill();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_drawSectorLines(ctx) {
|
|
275
|
+
const TWO_PI = Math.PI * 2;
|
|
276
|
+
|
|
277
|
+
ctx.strokeStyle = this._colors.sectorLine;
|
|
278
|
+
ctx.lineWidth = 0.5;
|
|
279
|
+
for (let s = 0; s < NUM_SECTORS; s++) {
|
|
280
|
+
const a = (s / NUM_SECTORS) * TWO_PI;
|
|
281
|
+
ctx.beginPath();
|
|
282
|
+
ctx.moveTo(Math.cos(a) * TRACK_INNER, Math.sin(a) * TRACK_INNER);
|
|
283
|
+
ctx.lineTo(Math.cos(a) * TRACK_OUTER, Math.sin(a) * TRACK_OUTER);
|
|
284
|
+
ctx.stroke();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_drawIndexHole(ctx) {
|
|
289
|
+
// Red parallelogram across the hub ring, rotates with disk
|
|
290
|
+
const outerR = HUB_RING_OUTER;
|
|
291
|
+
const innerR = HUB_RING_INNER;
|
|
292
|
+
const outerHalf = 1.5; // shorter side (next to disk)
|
|
293
|
+
const innerHalf = 2.5; // slightly longer side (next to center hole)
|
|
294
|
+
|
|
295
|
+
ctx.save();
|
|
296
|
+
ctx.translate(CENTER_X, CENTER_Y);
|
|
297
|
+
ctx.rotate(this.angle);
|
|
298
|
+
|
|
299
|
+
ctx.beginPath();
|
|
300
|
+
ctx.moveTo(outerR, -outerHalf);
|
|
301
|
+
ctx.lineTo(outerR, outerHalf);
|
|
302
|
+
ctx.lineTo(innerR, innerHalf);
|
|
303
|
+
ctx.lineTo(innerR, -innerHalf);
|
|
304
|
+
ctx.closePath();
|
|
305
|
+
|
|
306
|
+
ctx.fillStyle = 'rgba(200,30,30,0.9)';
|
|
307
|
+
ctx.fill();
|
|
308
|
+
ctx.strokeStyle = 'rgba(120,10,10,0.6)';
|
|
309
|
+
ctx.lineWidth = 0.5;
|
|
310
|
+
ctx.stroke();
|
|
311
|
+
|
|
312
|
+
ctx.restore();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_drawHeadArm(ctx, quarterTrack, isActive, isWriteMode) {
|
|
316
|
+
const trackPos = quarterTrack / 4;
|
|
317
|
+
const headR = TRACK_OUTER - ((trackPos + 0.5) * TRACK_RANGE / NUM_TRACKS);
|
|
318
|
+
|
|
319
|
+
// Head color: green when reading, red when writing, grey when idle
|
|
320
|
+
if (isActive) {
|
|
321
|
+
ctx.fillStyle = isWriteMode
|
|
322
|
+
? 'rgba(220,40,40,0.85)'
|
|
323
|
+
: 'rgba(40,200,60,0.85)';
|
|
324
|
+
} else {
|
|
325
|
+
ctx.fillStyle = 'rgba(160,155,150,0.7)';
|
|
326
|
+
}
|
|
327
|
+
ctx.fillRect(CENTER_X - 3, CENTER_Y - headR - 2, 6, 4);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_drawHeadGlow(ctx, quarterTrack, isWriteMode) {
|
|
331
|
+
const trackPos = quarterTrack / 4;
|
|
332
|
+
const headR = TRACK_OUTER - ((trackPos + 0.5) * TRACK_RANGE / NUM_TRACKS);
|
|
333
|
+
|
|
334
|
+
ctx.fillStyle = isWriteMode
|
|
335
|
+
? 'rgba(220,40,40,0.4)'
|
|
336
|
+
: 'rgba(40,200,60,0.4)';
|
|
337
|
+
ctx.fillRect(CENTER_X - 4, CENTER_Y - headR - 3, 8, 6);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_drawHub(ctx) {
|
|
341
|
+
const c = this._colors;
|
|
342
|
+
|
|
343
|
+
// White reinforcement hub ring
|
|
344
|
+
ctx.beginPath();
|
|
345
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_RING_OUTER, 0, Math.PI * 2);
|
|
346
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_RING_INNER, Math.PI * 2, 0, true);
|
|
347
|
+
ctx.fillStyle = c.hubRing;
|
|
348
|
+
ctx.fill();
|
|
349
|
+
|
|
350
|
+
// Ring edge lines
|
|
351
|
+
ctx.beginPath();
|
|
352
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_RING_OUTER, 0, Math.PI * 2);
|
|
353
|
+
ctx.strokeStyle = c.hubEdge;
|
|
354
|
+
ctx.lineWidth = 0.5;
|
|
355
|
+
ctx.stroke();
|
|
356
|
+
|
|
357
|
+
ctx.beginPath();
|
|
358
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_RING_INNER, 0, Math.PI * 2);
|
|
359
|
+
ctx.strokeStyle = c.hubEdgeInner;
|
|
360
|
+
ctx.lineWidth = 0.5;
|
|
361
|
+
ctx.stroke();
|
|
362
|
+
|
|
363
|
+
// Center hole — see through to background
|
|
364
|
+
ctx.beginPath();
|
|
365
|
+
ctx.arc(CENTER_X, CENTER_Y, HUB_HOLE_RADIUS, 0, Math.PI * 2);
|
|
366
|
+
ctx.fillStyle = c.bg;
|
|
367
|
+
ctx.fill();
|
|
368
|
+
|
|
369
|
+
// Hole edge
|
|
370
|
+
ctx.strokeStyle = c.holeEdge;
|
|
371
|
+
ctx.lineWidth = 0.5;
|
|
372
|
+
ctx.stroke();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_tintColor(hex, strength) {
|
|
376
|
+
let r = 0, g = 0, b = 0;
|
|
377
|
+
if (hex.length === 7) {
|
|
378
|
+
r = parseInt(hex.slice(1, 3), 16);
|
|
379
|
+
g = parseInt(hex.slice(3, 5), 16);
|
|
380
|
+
b = parseInt(hex.slice(5, 7), 16);
|
|
381
|
+
}
|
|
382
|
+
const br = 0x88, bg = 0x85, bb = 0x80;
|
|
383
|
+
r = Math.round(r * strength + br * (1 - strength));
|
|
384
|
+
g = Math.round(g * strength + bg * (1 - strength));
|
|
385
|
+
b = Math.round(b * strength + bb * (1 - strength));
|
|
386
|
+
return `rgb(${r},${g},${b})`;
|
|
387
|
+
}
|
|
388
|
+
}
|