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.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,705 @@
1
+ /*
2
+ * webgl-renderer.js - WebGL renderer for emulator display
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export class WebGLRenderer {
9
+ constructor(canvas) {
10
+ this.canvas = canvas;
11
+ this.gl = null;
12
+ this.program = null;
13
+ this.texture = null;
14
+
15
+ // Burn-in resources
16
+ this.burnInProgram = null;
17
+ this.burnInFramebuffers = [null, null];
18
+ this.burnInTextures = [null, null];
19
+ this.currentBurnInIndex = 0;
20
+
21
+ // Edge overlay program (second pass)
22
+ this.edgeProgram = null;
23
+
24
+ // Selection overlay texture
25
+ this.selectionTexture = null;
26
+
27
+ // Texture dimensions
28
+ this.width = 560;
29
+ this.height = 384;
30
+
31
+ // CRT effect parameters (0.0 to 1.0 unless noted)
32
+ this.crtParams = {
33
+ // Screen geometry
34
+ curvature: 0.2,
35
+
36
+ // Scanlines and rasterization
37
+ scanlineIntensity: 0.03,
38
+ scanlineWidth: 0.25,
39
+ shadowMask: 0.3,
40
+
41
+ // Glow/bloom
42
+ glowIntensity: 0.0,
43
+ glowSpread: 0.5,
44
+
45
+ // Color adjustments
46
+ brightness: 1.0, // 0.5 to 1.5
47
+ contrast: 1.0, // 0.5 to 1.5
48
+ saturation: 1.0, // 0.0 to 2.0
49
+
50
+ // Vignette
51
+ vignette: 0.0,
52
+
53
+ // Chromatic aberration
54
+ rgbOffset: 0.0,
55
+
56
+ // New effects from cool-retro-term
57
+ staticNoise: 0.0, // Static noise/grain
58
+ flicker: 0.0, // Brightness flicker
59
+ jitter: 0.0, // Random pixel displacement
60
+ horizontalSync: 0.0, // Horizontal sync distortion
61
+ glowingLine: 0.0, // Moving scan beam
62
+ ambientLight: 0.0, // Screen surface reflection
63
+ burnIn: 0.0, // Phosphor persistence
64
+
65
+ // Screen border/overscan
66
+ overscan: 0.0, // Border around display content (0.0 to 1.0)
67
+
68
+ // No signal mode (TV static when off)
69
+ noSignal: 0.0, // 1.0 = full static, 0.0 = normal display
70
+
71
+ // Color bleed - vertical inter-scanline blending (CRT phosphor overlap)
72
+ colorBleed: 0.8, // 0.0 to 1.0
73
+
74
+ // NTSC color fringing (simulates chroma bandwidth limiting)
75
+ ntscFringing: 0.3, // 0.0 to 1.0
76
+
77
+ // Monochrome mode (0=color, 1=green, 2=amber, 3=white)
78
+ monochromeMode: 0,
79
+
80
+ // Corner radius for rounded screen corners (0.0 to 0.15)
81
+ cornerRadius: 0.02,
82
+
83
+ // Screen margin - insets content so rounded corners don't clip it
84
+ // Should be slightly larger than cornerRadius
85
+ screenMargin: 0.02,
86
+
87
+ // Edge highlight intensity (0.0 to 1.0)
88
+ edgeHighlight: 0.3,
89
+
90
+ // Beam position crosshair (-1.0 = off, 0.0–1.0 = normalized position)
91
+ beamY: -1.0,
92
+ beamX: -1.0,
93
+ };
94
+
95
+ // Time for animated effects
96
+ this.time = 0;
97
+
98
+ // Uniform locations
99
+ this.uniforms = {};
100
+ this.burnInUniforms = {};
101
+ }
102
+
103
+ async init() {
104
+ // Get WebGL context
105
+ const ctxAttrs = {
106
+ alpha: true,
107
+ premultipliedAlpha: false,
108
+ preserveDrawingBuffer: true,
109
+ };
110
+ this.gl =
111
+ this.canvas.getContext("webgl2", ctxAttrs) ||
112
+ this.canvas.getContext("webgl", ctxAttrs);
113
+ if (!this.gl) {
114
+ throw new Error("WebGL not supported");
115
+ }
116
+
117
+ const gl = this.gl;
118
+
119
+ // Load shader sources from files
120
+ const [vertexSource, fragmentSource, burnInSource, edgeSource] =
121
+ await Promise.all([
122
+ this.loadShader("shaders/vertex.glsl"),
123
+ this.loadShader("shaders/crt.glsl"),
124
+ this.loadShader("shaders/burnin.glsl"),
125
+ this.loadShader("shaders/edge.glsl"),
126
+ ]);
127
+
128
+ // Create main shaders
129
+ const vertexShader = this.compileShader(gl.VERTEX_SHADER, vertexSource);
130
+ const fragmentShader = this.compileShader(
131
+ gl.FRAGMENT_SHADER,
132
+ fragmentSource,
133
+ );
134
+
135
+ // Create main program
136
+ this.program = gl.createProgram();
137
+ gl.attachShader(this.program, vertexShader);
138
+ gl.attachShader(this.program, fragmentShader);
139
+ gl.linkProgram(this.program);
140
+
141
+ if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
142
+ throw new Error(
143
+ "Shader program failed to link: " + gl.getProgramInfoLog(this.program),
144
+ );
145
+ }
146
+
147
+ // Create burn-in shaders
148
+ const burnInVertexShader = this.compileShader(
149
+ gl.VERTEX_SHADER,
150
+ vertexSource,
151
+ );
152
+ const burnInFragmentShader = this.compileShader(
153
+ gl.FRAGMENT_SHADER,
154
+ burnInSource,
155
+ );
156
+
157
+ // Create burn-in program
158
+ this.burnInProgram = gl.createProgram();
159
+ gl.attachShader(this.burnInProgram, burnInVertexShader);
160
+ gl.attachShader(this.burnInProgram, burnInFragmentShader);
161
+ gl.linkProgram(this.burnInProgram);
162
+
163
+ if (!gl.getProgramParameter(this.burnInProgram, gl.LINK_STATUS)) {
164
+ throw new Error(
165
+ "Burn-in shader program failed to link: " +
166
+ gl.getProgramInfoLog(this.burnInProgram),
167
+ );
168
+ }
169
+
170
+ // Create edge overlay shaders
171
+ const edgeVertexShader = this.compileShader(gl.VERTEX_SHADER, vertexSource);
172
+ const edgeFragmentShader = this.compileShader(
173
+ gl.FRAGMENT_SHADER,
174
+ edgeSource,
175
+ );
176
+
177
+ // Create edge overlay program
178
+ this.edgeProgram = gl.createProgram();
179
+ gl.attachShader(this.edgeProgram, edgeVertexShader);
180
+ gl.attachShader(this.edgeProgram, edgeFragmentShader);
181
+ gl.linkProgram(this.edgeProgram);
182
+
183
+ if (!gl.getProgramParameter(this.edgeProgram, gl.LINK_STATUS)) {
184
+ throw new Error(
185
+ "Edge shader program failed to link: " +
186
+ gl.getProgramInfoLog(this.edgeProgram),
187
+ );
188
+ }
189
+
190
+ // Get attribute locations
191
+ this.positionLoc = gl.getAttribLocation(this.program, "a_position");
192
+ this.texCoordLoc = gl.getAttribLocation(this.program, "a_texCoord");
193
+
194
+ // Get burn-in attribute locations
195
+ this.burnInPositionLoc = gl.getAttribLocation(
196
+ this.burnInProgram,
197
+ "a_position",
198
+ );
199
+ this.burnInTexCoordLoc = gl.getAttribLocation(
200
+ this.burnInProgram,
201
+ "a_texCoord",
202
+ );
203
+
204
+ // Get edge overlay attribute locations
205
+ this.edgePositionLoc = gl.getAttribLocation(this.edgeProgram, "a_position");
206
+ this.edgeTexCoordLoc = gl.getAttribLocation(this.edgeProgram, "a_texCoord");
207
+
208
+ // Get all uniform locations for main program
209
+ this.uniforms = {
210
+ texture: gl.getUniformLocation(this.program, "u_texture"),
211
+ burnInTexture: gl.getUniformLocation(this.program, "u_burnInTexture"),
212
+ resolution: gl.getUniformLocation(this.program, "u_resolution"),
213
+ textureSize: gl.getUniformLocation(this.program, "u_textureSize"),
214
+ time: gl.getUniformLocation(this.program, "u_time"),
215
+ curvature: gl.getUniformLocation(this.program, "u_curvature"),
216
+ scanlineIntensity: gl.getUniformLocation(
217
+ this.program,
218
+ "u_scanlineIntensity",
219
+ ),
220
+ scanlineWidth: gl.getUniformLocation(this.program, "u_scanlineWidth"),
221
+ shadowMask: gl.getUniformLocation(this.program, "u_shadowMask"),
222
+ glowIntensity: gl.getUniformLocation(this.program, "u_glowIntensity"),
223
+ glowSpread: gl.getUniformLocation(this.program, "u_glowSpread"),
224
+ brightness: gl.getUniformLocation(this.program, "u_brightness"),
225
+ contrast: gl.getUniformLocation(this.program, "u_contrast"),
226
+ saturation: gl.getUniformLocation(this.program, "u_saturation"),
227
+ vignette: gl.getUniformLocation(this.program, "u_vignette"),
228
+ flicker: gl.getUniformLocation(this.program, "u_flicker"),
229
+ rgbOffset: gl.getUniformLocation(this.program, "u_rgbOffset"),
230
+ staticNoise: gl.getUniformLocation(this.program, "u_staticNoise"),
231
+ jitter: gl.getUniformLocation(this.program, "u_jitter"),
232
+ horizontalSync: gl.getUniformLocation(this.program, "u_horizontalSync"),
233
+ glowingLine: gl.getUniformLocation(this.program, "u_glowingLine"),
234
+ ambientLight: gl.getUniformLocation(this.program, "u_ambientLight"),
235
+ burnIn: gl.getUniformLocation(this.program, "u_burnIn"),
236
+ overscan: gl.getUniformLocation(this.program, "u_overscan"),
237
+ noSignal: gl.getUniformLocation(this.program, "u_noSignal"),
238
+ colorBleed: gl.getUniformLocation(this.program, "u_colorBleed"),
239
+ ntscFringing: gl.getUniformLocation(this.program, "u_ntscFringing"),
240
+ monochromeMode: gl.getUniformLocation(this.program, "u_monochromeMode"),
241
+ cornerRadius: gl.getUniformLocation(this.program, "u_cornerRadius"),
242
+ screenMargin: gl.getUniformLocation(this.program, "u_screenMargin"),
243
+ beamY: gl.getUniformLocation(this.program, "u_beamY"),
244
+ beamX: gl.getUniformLocation(this.program, "u_beamX"),
245
+ selectionTexture: gl.getUniformLocation(
246
+ this.program,
247
+ "u_selectionTexture",
248
+ ),
249
+ };
250
+
251
+ // Get burn-in program uniform locations
252
+ this.burnInUniforms = {
253
+ currentTexture: gl.getUniformLocation(
254
+ this.burnInProgram,
255
+ "u_currentTexture",
256
+ ),
257
+ previousTexture: gl.getUniformLocation(
258
+ this.burnInProgram,
259
+ "u_previousTexture",
260
+ ),
261
+ burnInDecay: gl.getUniformLocation(this.burnInProgram, "u_burnInDecay"),
262
+ };
263
+
264
+ // Get edge overlay program uniform locations
265
+ this.edgeUniforms = {
266
+ curvature: gl.getUniformLocation(this.edgeProgram, "u_curvature"),
267
+ cornerRadius: gl.getUniformLocation(this.edgeProgram, "u_cornerRadius"),
268
+ edgeHighlight: gl.getUniformLocation(this.edgeProgram, "u_edgeHighlight"),
269
+ textureSize: gl.getUniformLocation(this.edgeProgram, "u_textureSize"),
270
+ resolution: gl.getUniformLocation(this.edgeProgram, "u_resolution"),
271
+ };
272
+
273
+ // Create vertex buffer (full-screen quad)
274
+ // Format: x, y, u, v - texture coords are flipped for screen rendering
275
+ const positions = new Float32Array([
276
+ -1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0,
277
+ ]);
278
+
279
+ this.vertexBuffer = gl.createBuffer();
280
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
281
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
282
+
283
+ // Create vertex buffer for framebuffer rendering (non-flipped texture coords)
284
+ const fbPositions = new Float32Array([
285
+ -1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1,
286
+ ]);
287
+
288
+ this.fbVertexBuffer = gl.createBuffer();
289
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fbVertexBuffer);
290
+ gl.bufferData(gl.ARRAY_BUFFER, fbPositions, gl.STATIC_DRAW);
291
+
292
+ // Create main texture
293
+ this.texture = gl.createTexture();
294
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
295
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
296
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
297
+
298
+ // Default to nearest neighbor filtering (sharp pixels)
299
+ this.useNearestFilter = true;
300
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
301
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
302
+
303
+ // Initialize with empty texture
304
+ const emptyData = new Uint8Array(this.width * this.height * 4);
305
+ gl.texImage2D(
306
+ gl.TEXTURE_2D,
307
+ 0,
308
+ gl.RGBA,
309
+ this.width,
310
+ this.height,
311
+ 0,
312
+ gl.RGBA,
313
+ gl.UNSIGNED_BYTE,
314
+ emptyData,
315
+ );
316
+
317
+ // Create burn-in framebuffers and textures (ping-pong)
318
+ for (let i = 0; i < 2; i++) {
319
+ this.burnInTextures[i] = gl.createTexture();
320
+ gl.bindTexture(gl.TEXTURE_2D, this.burnInTextures[i]);
321
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
322
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
323
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
324
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
325
+ gl.texImage2D(
326
+ gl.TEXTURE_2D,
327
+ 0,
328
+ gl.RGBA,
329
+ this.width,
330
+ this.height,
331
+ 0,
332
+ gl.RGBA,
333
+ gl.UNSIGNED_BYTE,
334
+ emptyData,
335
+ );
336
+
337
+ this.burnInFramebuffers[i] = gl.createFramebuffer();
338
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.burnInFramebuffers[i]);
339
+ gl.framebufferTexture2D(
340
+ gl.FRAMEBUFFER,
341
+ gl.COLOR_ATTACHMENT0,
342
+ gl.TEXTURE_2D,
343
+ this.burnInTextures[i],
344
+ 0,
345
+ );
346
+ }
347
+
348
+ // Unbind framebuffer
349
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
350
+
351
+ // Create selection overlay texture
352
+ this.selectionTexture = gl.createTexture();
353
+ gl.bindTexture(gl.TEXTURE_2D, this.selectionTexture);
354
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
355
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
356
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
357
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
358
+ gl.texImage2D(
359
+ gl.TEXTURE_2D,
360
+ 0,
361
+ gl.RGBA,
362
+ this.width,
363
+ this.height,
364
+ 0,
365
+ gl.RGBA,
366
+ gl.UNSIGNED_BYTE,
367
+ emptyData,
368
+ );
369
+
370
+ // Set initial canvas size if not already set
371
+ if (!this.canvas.width || !this.canvas.height) {
372
+ this.canvas.width = this.width;
373
+ this.canvas.height = this.height;
374
+ }
375
+
376
+ // Set viewport
377
+ gl.viewport(0, 0, this.canvas.width, this.canvas.height);
378
+
379
+ // Enable blending for rounded corners transparency
380
+ gl.enable(gl.BLEND);
381
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
382
+ }
383
+
384
+ compileShader(type, source) {
385
+ const gl = this.gl;
386
+ const shader = gl.createShader(type);
387
+ gl.shaderSource(shader, source);
388
+ gl.compileShader(shader);
389
+
390
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
391
+ throw new Error(
392
+ "Shader compilation failed: " + gl.getShaderInfoLog(shader),
393
+ );
394
+ }
395
+
396
+ return shader;
397
+ }
398
+
399
+ updateTexture(data) {
400
+ const gl = this.gl;
401
+
402
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
403
+ gl.texSubImage2D(
404
+ gl.TEXTURE_2D,
405
+ 0,
406
+ 0,
407
+ 0,
408
+ this.width,
409
+ this.height,
410
+ gl.RGBA,
411
+ gl.UNSIGNED_BYTE,
412
+ data,
413
+ );
414
+ }
415
+
416
+ updateSelectionTexture(canvas) {
417
+ const gl = this.gl;
418
+ gl.bindTexture(gl.TEXTURE_2D, this.selectionTexture);
419
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
420
+ }
421
+
422
+ updateBurnIn() {
423
+ const gl = this.gl;
424
+
425
+ if (this.crtParams.burnIn < 0.001) return;
426
+
427
+ // Swap buffers
428
+ const prevIndex = this.currentBurnInIndex;
429
+ this.currentBurnInIndex = 1 - this.currentBurnInIndex;
430
+ const currIndex = this.currentBurnInIndex;
431
+
432
+ // Render to current burn-in buffer
433
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.burnInFramebuffers[currIndex]);
434
+ gl.viewport(0, 0, this.width, this.height);
435
+
436
+ gl.useProgram(this.burnInProgram);
437
+
438
+ // Bind framebuffer vertex buffer (non-flipped texture coords)
439
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fbVertexBuffer);
440
+ gl.enableVertexAttribArray(this.burnInPositionLoc);
441
+ gl.vertexAttribPointer(this.burnInPositionLoc, 2, gl.FLOAT, false, 16, 0);
442
+ gl.enableVertexAttribArray(this.burnInTexCoordLoc);
443
+ gl.vertexAttribPointer(this.burnInTexCoordLoc, 2, gl.FLOAT, false, 16, 8);
444
+
445
+ // Bind current frame texture
446
+ gl.activeTexture(gl.TEXTURE0);
447
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
448
+ gl.uniform1i(this.burnInUniforms.currentTexture, 0);
449
+
450
+ // Bind previous burn-in texture
451
+ gl.activeTexture(gl.TEXTURE1);
452
+ gl.bindTexture(gl.TEXTURE_2D, this.burnInTextures[prevIndex]);
453
+ gl.uniform1i(this.burnInUniforms.previousTexture, 1);
454
+
455
+ // Set decay rate - higher burnIn = slower decay
456
+ const decayRate = 0.02 + (1.0 - this.crtParams.burnIn) * 0.08;
457
+ gl.uniform1f(this.burnInUniforms.burnInDecay, decayRate);
458
+
459
+ // Draw
460
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
461
+
462
+ // Unbind framebuffer and restore viewport
463
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
464
+ gl.viewport(0, 0, this.canvas.width, this.canvas.height);
465
+ }
466
+
467
+ draw() {
468
+ const gl = this.gl;
469
+
470
+ // Apply any pending canvas resize right before painting so the
471
+ // buffer clear and the redraw happen in the same frame (no flicker).
472
+ if (this._pendingWidth !== undefined) {
473
+ const pw = this._pendingWidth;
474
+ const ph = this._pendingHeight;
475
+ this._pendingWidth = undefined;
476
+ this._pendingHeight = undefined;
477
+ if (this.canvas.width !== pw || this.canvas.height !== ph) {
478
+ this.canvas.width = pw;
479
+ this.canvas.height = ph;
480
+ gl.viewport(0, 0, pw, ph);
481
+ }
482
+ }
483
+
484
+ // Update time for animated effects
485
+ this.time += 0.016; // Approximately 60fps
486
+
487
+ // Update burn-in accumulation
488
+ this.updateBurnIn();
489
+
490
+ gl.clearColor(0, 0, 0, 0); // Black background
491
+ gl.clear(gl.COLOR_BUFFER_BIT);
492
+
493
+ gl.useProgram(this.program);
494
+
495
+ // Bind vertex buffer
496
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
497
+
498
+ // Position attribute
499
+ gl.enableVertexAttribArray(this.positionLoc);
500
+ gl.vertexAttribPointer(this.positionLoc, 2, gl.FLOAT, false, 16, 0);
501
+
502
+ // TexCoord attribute
503
+ gl.enableVertexAttribArray(this.texCoordLoc);
504
+ gl.vertexAttribPointer(this.texCoordLoc, 2, gl.FLOAT, false, 16, 8);
505
+
506
+ // Bind main texture
507
+ gl.activeTexture(gl.TEXTURE0);
508
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
509
+ gl.uniform1i(this.uniforms.texture, 0);
510
+
511
+ // Bind burn-in texture
512
+ gl.activeTexture(gl.TEXTURE1);
513
+ gl.bindTexture(gl.TEXTURE_2D, this.burnInTextures[this.currentBurnInIndex]);
514
+ gl.uniform1i(this.uniforms.burnInTexture, 1);
515
+
516
+ // Bind selection overlay texture
517
+ gl.activeTexture(gl.TEXTURE2);
518
+ gl.bindTexture(gl.TEXTURE_2D, this.selectionTexture);
519
+ gl.uniform1i(this.uniforms.selectionTexture, 2);
520
+
521
+ // Set all uniforms
522
+ gl.uniform2f(
523
+ this.uniforms.resolution,
524
+ this.canvas.width,
525
+ this.canvas.height,
526
+ );
527
+ gl.uniform2f(this.uniforms.textureSize, this.width, this.height);
528
+ gl.uniform1f(this.uniforms.time, this.time);
529
+ gl.uniform1f(this.uniforms.curvature, this.crtParams.curvature);
530
+ gl.uniform1f(
531
+ this.uniforms.scanlineIntensity,
532
+ this.crtParams.scanlineIntensity,
533
+ );
534
+ gl.uniform1f(this.uniforms.scanlineWidth, this.crtParams.scanlineWidth);
535
+ gl.uniform1f(this.uniforms.shadowMask, this.crtParams.shadowMask);
536
+ gl.uniform1f(this.uniforms.glowIntensity, this.crtParams.glowIntensity);
537
+ gl.uniform1f(this.uniforms.glowSpread, this.crtParams.glowSpread);
538
+ gl.uniform1f(this.uniforms.brightness, this.crtParams.brightness);
539
+ gl.uniform1f(this.uniforms.contrast, this.crtParams.contrast);
540
+ gl.uniform1f(this.uniforms.saturation, this.crtParams.saturation);
541
+ gl.uniform1f(this.uniforms.vignette, this.crtParams.vignette);
542
+ gl.uniform1f(this.uniforms.flicker, this.crtParams.flicker);
543
+ gl.uniform1f(this.uniforms.rgbOffset, this.crtParams.rgbOffset);
544
+ gl.uniform1f(this.uniforms.staticNoise, this.crtParams.staticNoise);
545
+ gl.uniform1f(this.uniforms.jitter, this.crtParams.jitter);
546
+ gl.uniform1f(this.uniforms.horizontalSync, this.crtParams.horizontalSync);
547
+ gl.uniform1f(this.uniforms.glowingLine, this.crtParams.glowingLine);
548
+ gl.uniform1f(this.uniforms.ambientLight, this.crtParams.ambientLight);
549
+ gl.uniform1f(this.uniforms.burnIn, this.crtParams.burnIn);
550
+ gl.uniform1f(this.uniforms.overscan, this.crtParams.overscan);
551
+ gl.uniform1f(this.uniforms.noSignal, this.crtParams.noSignal);
552
+ gl.uniform1f(this.uniforms.colorBleed, this.crtParams.colorBleed);
553
+ gl.uniform1f(this.uniforms.ntscFringing, this.crtParams.ntscFringing);
554
+ gl.uniform1i(this.uniforms.monochromeMode, this.crtParams.monochromeMode);
555
+ gl.uniform1f(this.uniforms.cornerRadius, this.crtParams.cornerRadius);
556
+ gl.uniform1f(this.uniforms.screenMargin, this.crtParams.screenMargin);
557
+ gl.uniform1f(this.uniforms.beamY, this.crtParams.beamY);
558
+ gl.uniform1f(this.uniforms.beamX, this.crtParams.beamX);
559
+
560
+ // Draw main CRT pass
561
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
562
+
563
+ // --- Second pass: edge overlay (unaffected by CRT effects) ---
564
+ if (this.crtParams.edgeHighlight > 0.001) {
565
+ gl.useProgram(this.edgeProgram);
566
+
567
+ // Reuse the same vertex buffer (same full-screen quad)
568
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
569
+ gl.enableVertexAttribArray(this.edgePositionLoc);
570
+ gl.vertexAttribPointer(this.edgePositionLoc, 2, gl.FLOAT, false, 16, 0);
571
+ gl.enableVertexAttribArray(this.edgeTexCoordLoc);
572
+ gl.vertexAttribPointer(this.edgeTexCoordLoc, 2, gl.FLOAT, false, 16, 8);
573
+
574
+ // Set edge uniforms
575
+ gl.uniform1f(this.edgeUniforms.curvature, this.crtParams.curvature);
576
+ gl.uniform1f(this.edgeUniforms.cornerRadius, this.crtParams.cornerRadius);
577
+ gl.uniform1f(
578
+ this.edgeUniforms.edgeHighlight,
579
+ this.crtParams.edgeHighlight,
580
+ );
581
+ gl.uniform2f(this.edgeUniforms.textureSize, this.width, this.height);
582
+ gl.uniform2f(
583
+ this.edgeUniforms.resolution,
584
+ this.canvas.width,
585
+ this.canvas.height,
586
+ );
587
+
588
+ // Draw edge overlay (alpha-blended on top of CRT output)
589
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
590
+ }
591
+ }
592
+
593
+ clear() {
594
+ const gl = this.gl;
595
+
596
+ // Clear the texture to black
597
+ const emptyData = new Uint8Array(this.width * this.height * 4);
598
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
599
+ gl.texSubImage2D(
600
+ gl.TEXTURE_2D,
601
+ 0,
602
+ 0,
603
+ 0,
604
+ this.width,
605
+ this.height,
606
+ gl.RGBA,
607
+ gl.UNSIGNED_BYTE,
608
+ emptyData,
609
+ );
610
+
611
+ // Clear burn-in buffers
612
+ for (let i = 0; i < 2; i++) {
613
+ gl.bindTexture(gl.TEXTURE_2D, this.burnInTextures[i]);
614
+ gl.texSubImage2D(
615
+ gl.TEXTURE_2D,
616
+ 0,
617
+ 0,
618
+ 0,
619
+ this.width,
620
+ this.height,
621
+ gl.RGBA,
622
+ gl.UNSIGNED_BYTE,
623
+ emptyData,
624
+ );
625
+ }
626
+
627
+ // Clear and redraw
628
+ gl.clearColor(0, 0, 0, 0); // Black background
629
+ gl.clear(gl.COLOR_BUFFER_BIT);
630
+ this.draw();
631
+ }
632
+
633
+ // Set individual CRT parameter
634
+ setParam(name, value) {
635
+ if (name in this.crtParams) {
636
+ this.crtParams[name] = value;
637
+ }
638
+ }
639
+
640
+ // Set multiple CRT parameters at once
641
+ setParams(params) {
642
+ for (const [name, value] of Object.entries(params)) {
643
+ if (name in this.crtParams) {
644
+ this.crtParams[name] = value;
645
+ }
646
+ }
647
+ }
648
+
649
+ // Set texture filtering mode
650
+ setNearestFilter(enabled) {
651
+ const gl = this.gl;
652
+ this.useNearestFilter = enabled;
653
+
654
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
655
+ if (enabled) {
656
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
657
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
658
+ } else {
659
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
660
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
661
+ }
662
+ }
663
+
664
+ // Legacy method for compatibility
665
+ setCRTEnabled(enabled) {
666
+ // Apply preset CRT settings
667
+ if (enabled) {
668
+ this.crtParams.curvature = 0.3;
669
+ this.crtParams.scanlineIntensity = 0.3;
670
+ this.crtParams.shadowMask = 0.2;
671
+ this.crtParams.vignette = 0.2;
672
+ this.crtParams.glowIntensity = 0.1;
673
+ } else {
674
+ this.crtParams.curvature = 0;
675
+ this.crtParams.scanlineIntensity = 0;
676
+ this.crtParams.shadowMask = 0;
677
+ this.crtParams.vignette = 0;
678
+ this.crtParams.glowIntensity = 0;
679
+ }
680
+ }
681
+
682
+ // Set no-signal mode (TV static when emulator is off)
683
+ setNoSignal(enabled) {
684
+ this.crtParams.noSignal = enabled ? 1.0 : 0.0;
685
+ }
686
+
687
+ resize(width, height) {
688
+ // Defer the actual canvas buffer resize to the next draw() call.
689
+ // Setting canvas.width/height clears the WebGL drawing buffer and forces
690
+ // a GPU reallocation. During a drag-resize this is called on every
691
+ // mousemove, but draw() only runs once per rAF. Deferring keeps the
692
+ // buffer clear and the repaint in the same frame, eliminating flicker.
693
+ const dpr = window.devicePixelRatio || 1;
694
+ this._pendingWidth = Math.floor(width * dpr);
695
+ this._pendingHeight = Math.floor(height * dpr);
696
+ }
697
+
698
+ async loadShader(path) {
699
+ const response = await fetch(path);
700
+ if (!response.ok) {
701
+ throw new Error(`Failed to load shader: ${path}`);
702
+ }
703
+ return response.text();
704
+ }
705
+ }