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,706 @@
1
+ // Fragment shader with comprehensive CRT effects
2
+ // Inspired by cool-retro-term (https://github.com/Swordfish90/cool-retro-term)
3
+
4
+ precision highp float;
5
+
6
+ uniform sampler2D u_texture;
7
+ uniform sampler2D u_burnInTexture;
8
+ uniform sampler2D u_selectionTexture;
9
+ uniform vec2 u_resolution;
10
+ uniform vec2 u_textureSize;
11
+ uniform float u_time;
12
+
13
+ // CRT effect uniforms
14
+ uniform float u_curvature;
15
+ uniform float u_scanlineIntensity;
16
+ uniform float u_scanlineWidth;
17
+ uniform float u_shadowMask;
18
+ uniform float u_glowIntensity;
19
+ uniform float u_glowSpread;
20
+ uniform float u_brightness;
21
+ uniform float u_contrast;
22
+ uniform float u_saturation;
23
+ uniform float u_vignette;
24
+ uniform float u_flicker;
25
+ uniform float u_rgbOffset;
26
+
27
+ // New effect uniforms
28
+ uniform float u_staticNoise;
29
+ uniform float u_jitter;
30
+ uniform float u_horizontalSync;
31
+ uniform float u_glowingLine;
32
+ uniform float u_ambientLight;
33
+ uniform float u_burnIn;
34
+ uniform float u_overscan;
35
+ uniform float u_noSignal;
36
+
37
+ // NTSC fringing effect
38
+ uniform float u_ntscFringing;
39
+
40
+ // Color bleed - vertical inter-scanline blending (simulates CRT phosphor overlap)
41
+ uniform float u_colorBleed;
42
+
43
+ // Monochrome mode (0=color, 1=green, 2=amber, 3=white)
44
+ uniform int u_monochromeMode;
45
+
46
+ // Corner radius for rounded screen corners
47
+ uniform float u_cornerRadius;
48
+
49
+ // Beam position crosshair overlay (-1.0 = off, 0.0–1.0 = normalized position)
50
+ uniform float u_beamY;
51
+ uniform float u_beamX;
52
+
53
+ // Screen margin/padding for rounded corners (content is inset by this amount)
54
+ uniform float u_screenMargin;
55
+
56
+ varying vec2 v_texCoord;
57
+
58
+ // Constants
59
+ const float PI = 3.14159265359;
60
+
61
+ // ============================================
62
+ // Utility functions
63
+ // ============================================
64
+
65
+ float hash12(vec2 p) {
66
+ vec3 p3 = fract(vec3(p.xyx) * 0.1031);
67
+ p3 += dot(p3, p3.yzx + 33.33);
68
+ return fract((p3.x + p3.y) * p3.z);
69
+ }
70
+
71
+ float rgb2grey(vec3 v) {
72
+ return dot(v, vec3(0.21, 0.72, 0.07));
73
+ }
74
+
75
+ // ============================================
76
+ // Screen overscan/border
77
+ // ============================================
78
+
79
+ vec2 applyOverscan(vec2 uv) {
80
+ if (u_overscan < 0.001) return uv;
81
+
82
+ // Scale UV coordinates inward to create a border
83
+ // overscan of 1.0 = 10% border on each side (content fills 80% of screen)
84
+ float borderSize = u_overscan * 0.1;
85
+ float scale = 1.0 - (borderSize * 2.0);
86
+
87
+ // Scale from center
88
+ vec2 centered = uv - 0.5;
89
+ vec2 scaled = centered / scale;
90
+ return scaled + 0.5;
91
+ }
92
+
93
+ // ============================================
94
+ // Screen curvature (pincushion distortion)
95
+ // ============================================
96
+
97
+ vec2 curveUV(vec2 uv) {
98
+ if (u_curvature < 0.001) return uv;
99
+
100
+ vec2 cc = uv - 0.5;
101
+ float dist = dot(cc, cc);
102
+ float distortion = dist * u_curvature * 0.5;
103
+ vec2 curved = uv + cc * distortion;
104
+
105
+ return curved;
106
+ }
107
+
108
+ // ============================================
109
+ // Horizontal sync distortion
110
+ // ============================================
111
+
112
+ vec2 applyHorizontalSync(vec2 uv, float time) {
113
+ if (u_horizontalSync < 0.001) return uv;
114
+
115
+ float randVal = hash12(vec2(floor(time * 0.5), 0.0));
116
+ if (randVal > u_horizontalSync) return uv;
117
+
118
+ float distortionFreq = mix(4.0, 40.0, hash12(vec2(time * 0.1, 1.0)));
119
+ float distortionScale = u_horizontalSync * 0.02 * randVal;
120
+ float wave = sin((uv.y + time * 0.01) * distortionFreq);
121
+ uv.x += wave * distortionScale;
122
+
123
+ return uv;
124
+ }
125
+
126
+ // ============================================
127
+ // Jitter effect
128
+ // ============================================
129
+
130
+ vec2 applyJitter(vec2 uv, float time) {
131
+ if (u_jitter < 0.001) return uv;
132
+
133
+ vec2 noiseCoord = uv * 100.0 + vec2(time * 10.0, time * 7.0);
134
+ vec2 offset = vec2(
135
+ hash12(noiseCoord) - 0.5,
136
+ hash12(noiseCoord + vec2(100.0, 0.0)) - 0.5
137
+ );
138
+
139
+ return uv + offset * u_jitter * 0.005;
140
+ }
141
+
142
+ // ============================================
143
+ // Static noise effect (TV static style)
144
+ // ============================================
145
+
146
+ float staticNoise(vec2 uv, float time) {
147
+ if (u_staticNoise < 0.001) return 0.0;
148
+
149
+ // Blocky TV static - sized for authentic CRT look
150
+ vec2 blockSize = vec2(1.0, 1.0);
151
+ vec2 pixelCoord = floor(uv * u_textureSize / blockSize);
152
+
153
+ // Animate the noise - change every frame
154
+ vec2 noiseCoord = pixelCoord + vec2(
155
+ floor(time * 30.0) * 17.0,
156
+ floor(time * 30.0) * 31.0
157
+ );
158
+
159
+ float noise = hash12(noiseCoord);
160
+
161
+ // Add some scanline-like horizontal banding for authenticity
162
+ float scanBand = hash12(vec2(pixelCoord.y, floor(time * 15.0)));
163
+ noise = mix(noise, scanBand, 0.3);
164
+
165
+ // Slight vignette on the noise
166
+ vec2 cc = uv - 0.5;
167
+ float dist = length(cc);
168
+ float vignette = 1.0 - dist * 0.5;
169
+
170
+ return noise * u_staticNoise * vignette;
171
+ }
172
+
173
+ // ============================================
174
+ // No signal TV static (full screen)
175
+ // ============================================
176
+
177
+ vec3 noSignalStatic(vec2 uv, float time) {
178
+ // Blocky TV static - sized for authentic CRT look
179
+ vec2 blockSize = vec2(2.0, 2.0);
180
+ vec2 pixelCoord = floor(uv * u_textureSize / blockSize);
181
+
182
+ // Animate for that classic TV static feel
183
+ float frameTime = floor(time * 50.0);
184
+ vec2 noiseCoord = pixelCoord + vec2(frameTime * 17.0, frameTime * 31.0);
185
+
186
+ // Base noise
187
+ float noise = hash12(noiseCoord);
188
+
189
+ // Horizontal banding - occasional brighter/darker scanlines
190
+ float bandNoise = hash12(vec2(pixelCoord.y * 0.1, frameTime * 0.5));
191
+ float band = smoothstep(0.4, 0.7, bandNoise);
192
+ noise = mix(noise * 0.7, noise * 1.2, band);
193
+
194
+ // Occasional horizontal interference lines
195
+ float lineNoise = hash12(vec2(frameTime, 0.0));
196
+ if (lineNoise > 0.95) {
197
+ float lineY = hash12(vec2(frameTime, 1.0));
198
+ float lineDist = abs(uv.y - lineY);
199
+ if (lineDist < 0.02) {
200
+ noise = mix(noise, 1.0, (0.02 - lineDist) / 0.02 * 0.5);
201
+ }
202
+ }
203
+
204
+ // Slight brightness variation over time
205
+ float brightnessFlicker = 0.85 + hash12(vec2(frameTime * 0.1, 0.0)) * 0.3;
206
+ noise *= brightnessFlicker;
207
+
208
+ // Vignette
209
+ vec2 cc = uv - 0.5;
210
+ float dist = length(cc);
211
+ float vignette = 1.0 - dist * 0.0;
212
+ noise *= vignette;
213
+
214
+ // Clamp and return grayscale - reduced brightness for less dazzling effect
215
+ noise = clamp(noise, 0.0, 1.0) * 0.25;
216
+ return vec3(noise);
217
+ }
218
+
219
+ // ============================================
220
+ // Flicker effect
221
+ // ============================================
222
+
223
+ float flicker(float time) {
224
+ if (u_flicker < 0.001) return 1.0;
225
+
226
+ float noiseVal = hash12(vec2(floor(time * 15.0), 0.0));
227
+ return 1.0 + (noiseVal - 0.5) * u_flicker * 0.15;
228
+ }
229
+
230
+ // ============================================
231
+ // Glowing line effect (scanning beam)
232
+ // ============================================
233
+
234
+ float glowingLine(vec2 uv, float time) {
235
+ if (u_glowingLine < 0.001) return 0.0;
236
+
237
+ float beamPos = fract(time * 0.05);
238
+ float dist = abs(uv.y - beamPos);
239
+ float glow = smoothstep(0.1, 0.0, dist);
240
+
241
+ return glow * u_glowingLine * 0.3;
242
+ }
243
+
244
+ // ============================================
245
+ // Ambient light effect
246
+ // ============================================
247
+
248
+ vec3 applyAmbientLight(vec3 color, vec2 uv) {
249
+ if (u_ambientLight < 0.001) return color;
250
+
251
+ vec2 cc = uv - 0.5;
252
+ float dist = length(cc);
253
+ float ambient = (1.0 - dist) * (1.0 - dist);
254
+
255
+ return color + vec3(u_ambientLight * ambient * 0.15);
256
+ }
257
+
258
+ // ============================================
259
+ // Scanline effect
260
+ // ============================================
261
+
262
+ float scanlines(vec2 uv) {
263
+ if (u_scanlineIntensity < 0.001) return 1.0;
264
+
265
+ float scanline = sin(uv.y * u_textureSize.y * PI) * 0.5 + 0.5;
266
+ scanline = pow(scanline, u_scanlineWidth * 2.0 + 0.5);
267
+ return mix(1.0, scanline, u_scanlineIntensity);
268
+ }
269
+
270
+ // ============================================
271
+ // Shadow mask
272
+ // ============================================
273
+
274
+ vec3 shadowMask(vec2 uv) {
275
+ if (u_shadowMask < 0.001) return vec3(1.0);
276
+
277
+ vec2 pos = uv * u_resolution;
278
+ int px = int(mod(pos.x, 3.0));
279
+
280
+ vec3 mask;
281
+ if (px == 0) {
282
+ mask = vec3(1.0, 0.7, 0.7);
283
+ } else if (px == 1) {
284
+ mask = vec3(0.7, 1.0, 0.7);
285
+ } else {
286
+ mask = vec3(0.7, 0.7, 1.0);
287
+ }
288
+
289
+ return mix(vec3(1.0), mask, u_shadowMask);
290
+ }
291
+
292
+ // ============================================
293
+ // Vignette effect
294
+ // ============================================
295
+
296
+ float vignette(vec2 uv) {
297
+ if (u_vignette < 0.001) return 1.0;
298
+
299
+ vec2 center = uv - 0.5;
300
+ float dist = length(center);
301
+ float vig = 1.0 - dist * dist * u_vignette * 2.0;
302
+ return clamp(vig, 0.0, 1.0);
303
+ }
304
+
305
+ // ============================================
306
+ // Phosphor glow / bloom effect
307
+ // ============================================
308
+
309
+ vec3 glow(sampler2D tex, vec2 uv) {
310
+ if (u_glowIntensity < 0.001) return vec3(0.0);
311
+
312
+ vec3 bloom = vec3(0.0);
313
+ float spread = u_glowSpread * 0.01;
314
+
315
+ for (int x = -1; x <= 1; x++) {
316
+ for (int y = -1; y <= 1; y++) {
317
+ vec2 offset = vec2(float(x), float(y)) * spread;
318
+ bloom += texture2D(tex, uv + offset).rgb;
319
+ }
320
+ }
321
+ bloom /= 9.0;
322
+
323
+ return bloom * u_glowIntensity;
324
+ }
325
+
326
+ // ============================================
327
+ // RGB chromatic aberration
328
+ // ============================================
329
+
330
+ vec3 rgbShift(sampler2D tex, vec2 uv) {
331
+ if (u_rgbOffset < 0.001) return texture2D(tex, uv).rgb;
332
+
333
+ vec2 dir = uv - 0.5;
334
+ float offset = u_rgbOffset * 0.003;
335
+
336
+ vec2 rOffset = dir * offset;
337
+ vec2 bOffset = -dir * offset;
338
+
339
+ float r = texture2D(tex, uv + rOffset).r;
340
+ float g = texture2D(tex, uv).g;
341
+ float b = texture2D(tex, uv + bOffset).b;
342
+
343
+ return vec3(r, g, b);
344
+ }
345
+
346
+ // ============================================
347
+ // Color Bleed (vertical inter-scanline blending)
348
+ // Simulates CRT phosphor spot overlap where
349
+ // adjacent scanlines bleed into each other
350
+ // ============================================
351
+
352
+ vec3 colorBleed(sampler2D tex, vec2 uv, vec3 baseColor) {
353
+ if (u_colorBleed < 0.001) return baseColor;
354
+
355
+ vec2 texelSize = 1.0 / u_textureSize;
356
+
357
+ // 5-tap vertical kernel: sample 2 rows above and below
358
+ // Weights 1-1-2-1-1 (sum 6) chosen to perfectly cancel the common
359
+ // Apple II HIRES pattern where artifact colors alternate every 2 rows
360
+ // (each scanline is doubled, giving a BBVV period-4 pattern)
361
+ vec3 up2 = texture2D(tex, uv + vec2(0.0, -2.0 * texelSize.y)).rgb;
362
+ vec3 up1 = texture2D(tex, uv + vec2(0.0, -1.0 * texelSize.y)).rgb;
363
+ vec3 dn1 = texture2D(tex, uv + vec2(0.0, 1.0 * texelSize.y)).rgb;
364
+ vec3 dn2 = texture2D(tex, uv + vec2(0.0, 2.0 * texelSize.y)).rgb;
365
+
366
+ vec3 blended = (up2 + up1 + baseColor * 2.0 + dn1 + dn2) / 6.0;
367
+
368
+ return mix(baseColor, blended, u_colorBleed);
369
+ }
370
+
371
+ // ============================================
372
+ // NTSC Color Fringing
373
+ // Simulates the limited chroma bandwidth of NTSC
374
+ // causing color "ringing" at sharp edges
375
+ // ============================================
376
+
377
+ vec3 ntscFringing(sampler2D tex, vec2 uv, vec3 baseColor) {
378
+ if (u_ntscFringing < 0.001) return baseColor;
379
+
380
+ // Pixel size for sampling neighbors
381
+ vec2 pixelSize = 1.0 / u_textureSize;
382
+
383
+ // Sample neighboring pixels horizontally (NTSC fringing is horizontal)
384
+ vec3 left2 = texture2D(tex, uv + vec2(-2.0 * pixelSize.x, 0.0)).rgb;
385
+ vec3 left1 = texture2D(tex, uv + vec2(-1.0 * pixelSize.x, 0.0)).rgb;
386
+ vec3 right1 = texture2D(tex, uv + vec2(1.0 * pixelSize.x, 0.0)).rgb;
387
+ vec3 right2 = texture2D(tex, uv + vec2(2.0 * pixelSize.x, 0.0)).rgb;
388
+
389
+ // Calculate brightness (luma) for edge detection
390
+ float lumaCenter = rgb2grey(baseColor);
391
+ float lumaLeft1 = rgb2grey(left1);
392
+ float lumaLeft2 = rgb2grey(left2);
393
+ float lumaRight1 = rgb2grey(right1);
394
+ float lumaRight2 = rgb2grey(right2);
395
+
396
+ // Detect edges - looking for significant brightness transitions
397
+ float leftAvg = (lumaLeft1 + lumaLeft2) * 0.5;
398
+ float rightAvg = (lumaRight1 + lumaRight2) * 0.5;
399
+
400
+ // Edge strength: how much brighter is one side vs the other
401
+ float leftEdge = max(0.0, rightAvg - leftAvg); // Bright on right = left edge of bright area
402
+ float rightEdge = max(0.0, leftAvg - rightAvg); // Bright on left = right edge of bright area
403
+
404
+ // Only apply fringing where there's a significant edge
405
+ float edgeThreshold = 0.15;
406
+ leftEdge = smoothstep(edgeThreshold, edgeThreshold + 0.2, leftEdge);
407
+ rightEdge = smoothstep(edgeThreshold, edgeThreshold + 0.2, rightEdge);
408
+
409
+ // NTSC fringe colors (magenta on left edges, cyan on right edges)
410
+ vec3 magentaFringe = vec3(0.84, 0.26, 1.0); // Purple/Magenta
411
+ vec3 cyanFringe = vec3(0.42, 0.90, 0.72); // Aqua/Cyan
412
+
413
+ // Apply fringing with smooth blending
414
+ // Fringe is stronger on darker pixels near bright edges
415
+ float darkness = 1.0 - lumaCenter;
416
+ float fringeStrength = u_ntscFringing * darkness * 0.7;
417
+
418
+ vec3 result = baseColor;
419
+
420
+ // Left edge fringing (magenta)
421
+ if (leftEdge > 0.0) {
422
+ result = mix(result, magentaFringe, leftEdge * fringeStrength);
423
+ }
424
+
425
+ // Right edge fringing (cyan)
426
+ if (rightEdge > 0.0) {
427
+ result = mix(result, cyanFringe, rightEdge * fringeStrength);
428
+ }
429
+
430
+ // Add subtle horizontal color blur to simulate chroma bandwidth limiting
431
+ // This makes the overall color response smoother
432
+ vec3 blurredChroma = (left1 + baseColor + right1) / 3.0;
433
+ float chromaBlur = u_ntscFringing * 0.15;
434
+ result = mix(result, blurredChroma, chromaBlur);
435
+
436
+ return result;
437
+ }
438
+
439
+ // ============================================
440
+ // Color adjustment
441
+ // ============================================
442
+
443
+ vec3 adjustColor(vec3 color) {
444
+ color *= u_brightness;
445
+ color = (color - 0.5) * u_contrast + 0.5;
446
+ float gray = rgb2grey(color);
447
+ color = mix(vec3(gray), color, u_saturation);
448
+ return color;
449
+ }
450
+
451
+ // ============================================
452
+ // Monochrome mode
453
+ // ============================================
454
+
455
+ vec3 applyMonochrome(vec3 color) {
456
+ if (u_monochromeMode == 0) return color; // Color mode - no change
457
+
458
+ // Convert to grayscale using luminance
459
+ float gray = rgb2grey(color);
460
+
461
+ // Apply tint based on monochrome mode
462
+ if (u_monochromeMode == 1) {
463
+ // Green phosphor (classic Apple II monitor)
464
+ // P1 phosphor green: slightly blue-green tint
465
+ return vec3(gray * 0.2, gray * 1.0, gray * 0.2);
466
+ } else if (u_monochromeMode == 2) {
467
+ // Amber phosphor (common on IBM PCs)
468
+ // Warm orange-yellow tint
469
+ return vec3(gray * 1.0, gray * 0.75, gray * 0.2);
470
+ } else if (u_monochromeMode == 3) {
471
+ // White phosphor (paper white)
472
+ // Slight warm tint for authenticity
473
+ return vec3(gray * 1.0, gray * 1.0, gray * 0.9);
474
+ }
475
+
476
+ return color;
477
+ }
478
+
479
+ // ============================================
480
+ // Edge effects
481
+ // ============================================
482
+
483
+ float edgeFade(vec2 uv) {
484
+ vec2 edge = smoothstep(0.0, 0.005, uv) * smoothstep(0.0, 0.005, 1.0 - uv);
485
+ return mix(0.85, 1.0, edge.x * edge.y);
486
+ }
487
+
488
+ float smoothEdge(vec2 uv) {
489
+ if (u_curvature < 0.001 && u_cornerRadius < 0.001) return 1.0;
490
+
491
+ vec2 centered = uv - 0.5;
492
+ // Use explicit corner radius if set, otherwise derive from curvature
493
+ float cornerRadius = u_cornerRadius > 0.001 ? u_cornerRadius : u_curvature * 0.03;
494
+ vec2 cornerDist = abs(centered) - (0.5 - cornerRadius);
495
+ cornerDist = max(cornerDist, 0.0);
496
+ float corner = length(cornerDist) / cornerRadius;
497
+
498
+ return 1.0 - smoothstep(0.9, 1.0, corner);
499
+ }
500
+
501
+ // Rounded rectangle SDF for clean corner masking
502
+ float roundedRectAlpha(vec2 uv, float radius) {
503
+ if (radius < 0.001) return 1.0;
504
+
505
+ vec2 centered = abs(uv - 0.5);
506
+ vec2 cornerDist = centered - (0.5 - radius);
507
+
508
+ // Inside the rectangle (not in corner region)
509
+ if (cornerDist.x < 0.0 || cornerDist.y < 0.0) {
510
+ return 1.0;
511
+ }
512
+
513
+ // In corner region - use distance from corner arc
514
+ float dist = length(cornerDist);
515
+ // Smooth anti-aliased edge
516
+ return 1.0 - smoothstep(radius - 0.005, radius + 0.005, dist);
517
+ }
518
+
519
+ // Apply screen margin - scales content inward so corners don't clip it
520
+ vec2 applyScreenMargin(vec2 uv) {
521
+ if (u_screenMargin < 0.001) return uv;
522
+
523
+ // Scale UV from center to create margin
524
+ vec2 centered = uv - 0.5;
525
+ float scale = 1.0 / (1.0 - u_screenMargin * 2.0);
526
+ return centered * scale + 0.5;
527
+ }
528
+
529
+ // ============================================
530
+ // Beam position crosshair overlay
531
+ // ============================================
532
+
533
+ vec3 beamOverlay(vec2 uv) {
534
+ if (u_beamY < 0.0 && u_beamX < 0.0) return vec3(0.0);
535
+
536
+ // Line thickness in UV space (~1.5 pixels)
537
+ float lineW = 1.5 / u_textureSize.x;
538
+ float lineH = 1.5 / u_textureSize.y;
539
+
540
+ vec3 lineColor = vec3(1.0, 0.0, 0.0); // Red
541
+ float intensity = 0.0;
542
+
543
+ // Horizontal line at beamY
544
+ if (u_beamY >= 0.0 && u_beamY <= 1.0) {
545
+ float dy = abs(uv.y - u_beamY);
546
+ intensity += smoothstep(lineH, 0.0, dy) * 0.6;
547
+ }
548
+
549
+ // Vertical line at beamX
550
+ if (u_beamX >= 0.0 && u_beamX <= 1.0) {
551
+ float dx = abs(uv.x - u_beamX);
552
+ intensity += smoothstep(lineW, 0.0, dx) * 0.6;
553
+ }
554
+
555
+ return lineColor * min(intensity, 1.0);
556
+ }
557
+
558
+ // ============================================
559
+ // Main fragment shader
560
+ // ============================================
561
+
562
+ void main() {
563
+ vec2 uv = v_texCoord;
564
+
565
+ // Stable screen boundary from undistorted coordinates.
566
+ // The physical CRT mask doesn't wobble — only the beam does.
567
+ // All clipping and alpha use this so nothing renders outside the edge.
568
+ vec2 stableCurvedUV = curveUV(uv);
569
+
570
+ float cornerAlpha = roundedRectAlpha(stableCurvedUV, u_cornerRadius);
571
+ if (cornerAlpha < 0.001) {
572
+ gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
573
+ return;
574
+ }
575
+
576
+ float edgeFactor = smoothEdge(stableCurvedUV);
577
+ if (edgeFactor < 0.001) {
578
+ gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
579
+ return;
580
+ }
581
+
582
+ if (stableCurvedUV.x < 0.0 || stableCurvedUV.x > 1.0 || stableCurvedUV.y < 0.0 || stableCurvedUV.y > 1.0) {
583
+ gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
584
+ return;
585
+ }
586
+
587
+ // Apply signal distortions — the beam wobbles, the mask does not
588
+ vec2 distortedUV = applyHorizontalSync(uv, u_time);
589
+ distortedUV = applyJitter(distortedUV, u_time);
590
+ vec2 curvedUV = curveUV(distortedUV);
591
+
592
+ // Content coordinates use the distorted beam position
593
+ vec2 contentUV = applyOverscan(curvedUV);
594
+ contentUV = applyScreenMargin(contentUV);
595
+
596
+ // Dark bezel color for areas outside content
597
+ vec3 darkBezelColor = vec3(0.0); // Black
598
+
599
+ // Check if we're in the margin area (outside content but inside screen)
600
+ bool inMargin = contentUV.x < 0.0 || contentUV.x > 1.0 || contentUV.y < 0.0 || contentUV.y > 1.0;
601
+
602
+ // No signal mode - show TV static instead of emulator content
603
+ if (u_noSignal > 0.5) {
604
+ vec3 staticColor = noSignalStatic(curvedUV, u_time);
605
+
606
+ // Apply scanlines to static for authentic look
607
+ staticColor *= scanlines(curvedUV);
608
+
609
+ // Apply vignette
610
+ staticColor *= vignette(curvedUV);
611
+
612
+ // Apply edge fade for curved screens
613
+ if (u_curvature > 0.001) {
614
+ staticColor *= edgeFade(curvedUV);
615
+ }
616
+
617
+ float staticAlpha = cornerAlpha * edgeFactor;
618
+ gl_FragColor = vec4(staticColor, staticAlpha);
619
+ return;
620
+ }
621
+
622
+ // Get base color - dark bezel color for margin area, texture sample for content
623
+ vec3 color;
624
+ if (inMargin) {
625
+ color = darkBezelColor;
626
+ } else {
627
+ // Get base color with RGB shift
628
+ color = rgbShift(u_texture, contentUV);
629
+
630
+ // Apply vertical color bleed (CRT inter-scanline blending)
631
+ color = colorBleed(u_texture, contentUV, color);
632
+
633
+ // Apply NTSC color fringing only in colour mode
634
+ if (u_monochromeMode == 0) {
635
+ color = ntscFringing(u_texture, contentUV, color);
636
+ }
637
+ }
638
+
639
+ // Apply texture-based effects only for content area
640
+ if (!inMargin) {
641
+ // Blend text selection overlay (before burn-in and glow so CRT effects apply on top)
642
+ vec4 sel = texture2D(u_selectionTexture, contentUV);
643
+ if (sel.a > 0.0) {
644
+ color = mix(color, sel.rgb, sel.a);
645
+ }
646
+
647
+ // Apply burn-in from accumulation buffer
648
+ if (u_burnIn > 0.001) {
649
+ // Burn-in texture is stored in non-flipped coords, flip Y to match main texture
650
+ vec2 burnInCoord = vec2(contentUV.x, 1.0 - contentUV.y);
651
+ vec3 burnInColor = texture2D(u_burnInTexture, burnInCoord).rgb;
652
+ color = max(color, burnInColor * u_burnIn);
653
+ }
654
+
655
+ // Add phosphor glow
656
+ color += glow(u_texture, contentUV);
657
+ }
658
+
659
+ // Apply scanlines (use curvedUV for consistent scanlines across margin)
660
+ color *= scanlines(curvedUV);
661
+
662
+ // Apply shadow mask
663
+ color *= shadowMask(curvedUV);
664
+
665
+ // Apply color adjustments (brightness, contrast, saturation)
666
+ color = adjustColor(color);
667
+
668
+ // Apply monochrome mode (after color adjustments, before vignette)
669
+ color = applyMonochrome(color);
670
+
671
+ // Apply vignette
672
+ color *= vignette(curvedUV);
673
+
674
+ // Apply edge fade for curved screens (uses stable coords — physical screen property)
675
+ if (u_curvature > 0.001) {
676
+ color *= edgeFade(stableCurvedUV);
677
+ }
678
+
679
+ // Apply flicker
680
+ color *= flicker(u_time);
681
+
682
+ // Add glowing line
683
+ color += vec3(glowingLine(curvedUV, u_time));
684
+
685
+ // Add static noise
686
+ color += vec3(staticNoise(curvedUV, u_time));
687
+
688
+ // Apply ambient light
689
+ color = applyAmbientLight(color, curvedUV);
690
+
691
+ // Beam position crosshair (opaque overlay, stable UV — only curve applied)
692
+ {
693
+ vec2 beamUV = applyOverscan(stableCurvedUV);
694
+ beamUV = applyScreenMargin(beamUV);
695
+ vec3 beam = beamOverlay(beamUV);
696
+ color = mix(color, vec3(1.0, 0.0, 0.0), beam.r);
697
+ }
698
+
699
+ // Clamp final color
700
+ color = clamp(color, 0.0, 1.0);
701
+
702
+ // Alpha combines corner rounding and curvature edge fade
703
+ float alpha = cornerAlpha * edgeFactor;
704
+
705
+ gl_FragColor = vec4(color, alpha);
706
+ }