meltygui 0.1.0__py3-none-any.whl

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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,1302 @@
1
+ """
2
+ Built-in shader types and common shaders.
3
+
4
+ These are automatically registered when importing meltygui.
5
+ """
6
+
7
+ from meltygui.graphics.registry import register_shader_type
8
+ from meltygui.graphics.registry import register_shader
9
+ from meltygui.graphics.base import GLType
10
+
11
+
12
+ @register_shader
13
+ class ShadowCast:
14
+ """
15
+ Shadow cast using light transmission model.
16
+ Multiple shadows combine correctly via multiplication.
17
+ """
18
+ shader_type = 'standard'
19
+ offset_factor = 0.04
20
+ uniforms = {
21
+ 'light_dir': (GLType.VEC2, (-0.5 * offset_factor, 1.6 * offset_factor)),
22
+ 'height_scale': (GLType.FLOAT, 0.5),
23
+ 'blur_scale': (GLType.FLOAT, 0.3),
24
+ # CONTACT-HARDENING curve: how fast the penumbra grows with the
25
+ # fragment's distance from the caster's silhouette edge (measured
26
+ # in-shader by bisecting along light_dir, normalized 0 at contact,
27
+ # 1 at the shadow tip). blur_scale stays the magnitude at the far
28
+ # end; this shapes the ramp: < 1 blooms the blur rapidly just past
29
+ # the contact edge, 1 = linear growth, > 1 stays crisp for most of
30
+ # the shadow and only softens the tip. 0 = the legacy behavior
31
+ # (uniform blur across the whole shadow).
32
+ 'blur_exponent': (GLType.FLOAT, 1.0),
33
+ 'max_steps': (GLType.INT, 64),
34
+ 'blur_samples': (GLType.INT, 8),
35
+ # Occlusion each caster hit contributes before the depth gap decay:
36
+ # the base darkness of a shadow right at its caster (per-sample,
37
+ # accumulated into coverage).
38
+ 'hit_strength': (GLType.FLOAT, 0.7),
39
+ # How fast that contribution decays per unit of caster/receiver
40
+ # depth gap: higher = deep stacks fade their shadows out sooner
41
+ # (contribution clamps at 0, never lightens).
42
+ 'hit_falloff': (GLType.FLOAT, 14.0),
43
+ 'depth_bias': (GLType.FLOAT, 0.00),
44
+ 'surface_threshold': (GLType.FLOAT, -10.0),
45
+ 'min_height_diff': (GLType.FLOAT, 0.000),
46
+ 'shadow_strength': (GLType.FLOAT, 0.4),
47
+ # Scale applied to raw depth-texture reads. Helps the caller bind the
48
+ # UI's F16 rank mask directly instead of a pre-normalized RGBA8 copy:
49
+ # 8-bit quantization there was ~6.6 depth slices per quantum, so
50
+ # caster/receiver gaps rounded differently every time a window's
51
+ # depth slot changed and shadow intensity visibly wandered.
52
+ 'depth_scale': (GLType.FLOAT, 1.0),
53
+ 'texture_size': (GLType.VEC2, None),
54
+ }
55
+ fragment_code = """
56
+ void main() {
57
+ vec2 uv = v_texcoord;
58
+ float receiver_depth = texture(u_texture, uv).r * depth_scale;
59
+
60
+ if (receiver_depth < surface_threshold) {
61
+ fragColor = vec4(0.0, 0.0, 0.0, 0.0);
62
+ return;
63
+ }
64
+
65
+ vec2 light_normalized = normalize(light_dir);
66
+ float min_offset = 0.0;
67
+
68
+ // float min_offset = (0.35) * height_scale;
69
+ float depth_step = 1.0 / float(max_steps);
70
+
71
+ // Start with full light, each caster blocks some
72
+ float light = 1.0;
73
+ float max_height_diff = 0.0;
74
+
75
+ for (float i = 0.5; i <= 16.0; i++) {
76
+ float test_caster_depth = receiver_depth + float(i) * depth_step;
77
+
78
+ float height_diff = test_caster_depth - receiver_depth;
79
+
80
+ if (height_diff < min_height_diff) {
81
+ continue;
82
+ }
83
+
84
+ float shadow_offset = max(height_diff * height_scale, min_offset);
85
+ vec2 base_sample_pos = uv + light_normalized * shadow_offset;
86
+
87
+ // Contact-hardening: the penumbra should grow with the fragment's
88
+ // DISTANCE FROM THE CASTER EDGE along the shadow, not just the
89
+ // depth gap (which is constant across a flat caster's whole
90
+ // shadow, making the blur uniform from contact edge to tip). The
91
+ // caster lies toward +light_dir from any shadowed fragment, so
92
+ // bisect [0, shadow_offset] for the nearest sample inside the
93
+ // caster: fragments hugging the silhouette find it almost
94
+ // immediately (edge_dist ~ 0, crisp), fragments at the shadow tip
95
+ // only at full offset (edge_dist ~ 1, full blur). Gated on the
96
+ // base sample actually hitting the caster — penumbra-fringe
97
+ // fragments whose center sample misses keep edge_dist = 1 (they
98
+ // ARE the soft tail). Assumes a contiguous caster interval, true
99
+ // for the UI's convex rects/strips.
100
+ float edge_dist = 1.0;
101
+ float d_at_offset = texture(u_texture, base_sample_pos).r * depth_scale;
102
+ if (blur_exponent > 0.0
103
+ && d_at_offset >= test_caster_depth - depth_bias) {
104
+ float t_lo = 0.0;
105
+ float t_hi = shadow_offset;
106
+ for (int b = 0; b < 5; b++) {
107
+ float mid = 0.5 * (t_lo + t_hi);
108
+ float d_mid = texture(u_texture,
109
+ uv + light_normalized * mid).r
110
+ * depth_scale;
111
+ if (d_mid >= test_caster_depth - depth_bias) {
112
+ t_hi = mid;
113
+ } else {
114
+ t_lo = mid;
115
+ }
116
+ }
117
+ edge_dist = t_hi / max(shadow_offset, 1e-6);
118
+ }
119
+ // blur_exponent = 0 -> pow term is 1 -> legacy uniform blur.
120
+ float blur_radius = height_diff * blur_scale
121
+ * pow(max(edge_dist, 1e-4), blur_exponent);
122
+
123
+ float hits = 0.0;
124
+ float total_samples = 0.0;
125
+
126
+ for (int s = 0; s < blur_samples; s++) {
127
+ float angle = float(s) * 6.28318 / float(blur_samples);
128
+
129
+ for (int r = 0; r <= 2; r++) {
130
+ float radius = blur_radius * float(r) / 2.0;
131
+ vec2 offset = vec2(cos(angle), sin(angle)) * radius;
132
+ vec2 sample_pos = base_sample_pos + offset;
133
+
134
+ if (sample_pos.x < 0.0 || sample_pos.x > 1.0 ||
135
+ sample_pos.y < 0.0 || sample_pos.y > 1.0) {
136
+ total_samples += 1.0;
137
+ continue;
138
+ }
139
+
140
+ float scene_depth = texture(u_texture, sample_pos).r * depth_scale;
141
+
142
+ if (scene_depth >= test_caster_depth - depth_bias) {
143
+ // Clamped at 0 so a large falloff fades a slice's
144
+ // contribution out instead of going negative and
145
+ // eating other slices' coverage.
146
+ hits += max(0.0, hit_strength - height_diff * hit_falloff);
147
+ }
148
+ total_samples += 1.0;
149
+ }
150
+ }
151
+
152
+ float coverage = hits / total_samples;
153
+
154
+ if (coverage > 0.0) {
155
+ // Multiply remaining light by (1 - blockage)
156
+ // This correctly combines overlapping soft shadows
157
+ light *= (1.0 - coverage);
158
+ max_height_diff = max(max_height_diff, height_diff);
159
+ }
160
+ }
161
+
162
+ // Shadow is inverse of remaining light
163
+ float shadow_intensity = (1.0 - light) * shadow_strength;
164
+
165
+ fragColor = vec4(shadow_intensity, max_height_diff, 0.0, receiver_depth);
166
+ }
167
+ """
168
+
169
+ @register_shader
170
+ class ShadowBlur:
171
+ """
172
+ Pass 2: Distance-dependent blur of the shadow map.
173
+
174
+ Shadows that traveled farther get blurred more, creating soft penumbra.
175
+
176
+ Input: Output from ShadowCast
177
+ Output: Blurred shadow map (same format)
178
+ """
179
+ shader_type = 'standard'
180
+ uniforms = {
181
+ 'blur_scale': (GLType.FLOAT, 20.0), # Max blur radius in pixels
182
+ 'min_blur': (GLType.FLOAT, 1.0), # Minimum blur (keeps some softness)
183
+ 'texture_size': (GLType.VEC2, None),
184
+ }
185
+ fragment_code = """
186
+ void main() {
187
+ vec2 uv = v_texcoord;
188
+ vec2 texel = 1.0 / texture_size;
189
+
190
+ vec4 center = texture(u_texture, uv);
191
+ float travel_dist = center.g; // How far the shadow traveled
192
+ float receiver_depth = center.a;
193
+
194
+ // Blur radius scales with travel distance
195
+ float blur_radius = mix(min_blur, blur_scale, travel_dist);
196
+ int r = int(ceil(blur_radius));
197
+
198
+ if (r <= 0) {
199
+ fragColor = center;
200
+ return;
201
+ }
202
+
203
+ // Gaussian blur with variable radius
204
+ vec4 sum = vec4(0.0);
205
+ float total_weight = 0.0;
206
+ float sigma = blur_radius / 3.0;
207
+ float sigma2 = sigma * sigma;
208
+
209
+ for (int x = -r; x <= r; x++) {
210
+ for (int y = -r; y <= r; y++) {
211
+ vec2 offset = vec2(float(x), float(y));
212
+ vec2 sample_uv = uv + offset * texel;
213
+
214
+ vec4 sample_val = texture(u_texture, sample_uv);
215
+
216
+ // Only blur shadows that are on surfaces at similar depth
217
+ // This prevents shadow bleeding across depth discontinuities
218
+ float depth_diff = abs(sample_val.a - receiver_depth);
219
+ float depth_weight = exp(-depth_diff * 50.0);
220
+
221
+ // Gaussian weight
222
+ float dist2 = float(x*x + y*y);
223
+ float gauss_weight = exp(-dist2 / (2.0 * sigma2));
224
+
225
+ float weight = gauss_weight * depth_weight;
226
+ sum += sample_val * weight;
227
+ total_weight += weight;
228
+ }
229
+ }
230
+
231
+ fragColor = sum / total_weight;
232
+ }
233
+ """
234
+
235
+
236
+ @register_shader
237
+ class ShadowComposite:
238
+ """
239
+ Pass 3: Composite shadows onto the original image.
240
+
241
+ Input uniforms:
242
+ u_texture: Original color texture
243
+ shadow_map: Blurred shadow map from Pass 2
244
+ depth_map: Original depth map
245
+
246
+ Output: Final composited image with shadows
247
+ """
248
+ shader_type = 'standard'
249
+ uniforms = {
250
+ 'shadow_map': (GLType.SAMPLER2D, None),
251
+ 'depth_map': (GLType.SAMPLER2D, None),
252
+ 'shadow_opacity': (GLType.FLOAT, 1.0), # How dark shadows get
253
+ 'shadow_color': (GLType.VEC3, (0.0, 0.0, 0.0)), # Shadow tint
254
+ # Resolution of shadow_map. May be lower than the screen when the shadow
255
+ # map is downscaled for performance; used to drive the bilateral upsample.
256
+ 'shadow_size': (GLType.VEC2, None),
257
+ # How aggressively the upsample snaps the shadow to depth edges. Higher =
258
+ # crisper edges (less fringing), lower = softer. Depths are in [0, 1].
259
+ 'depth_sharpness': (GLType.FLOAT, 50.0),
260
+ # Same sample depth scale as ShadowCast's depth_scale - depth_map is the
261
+ # raw R16 depth mask, and s.a (receiver depth baked by ShadowCast) is
262
+ # already scaled, so the bilateral compare needs both in the same units.
263
+ 'depth_scale': (GLType.FLOAT, 1.0),
264
+ # Glow light buffer (TileCacheMasked add_glow marks, low-res RGBA16F):
265
+ # rgb = accumulated emitted light, already depth-gated per emitter at
266
+ # stamp time (the glow stamp shader samples the depth rank mask and
267
+ # only lands light between the emitter's root-window surface and its
268
+ # own depth). Composite-side the light cancels shadow where it falls
269
+ # (glow_shadow_cut) then adds its emission on top (glow_strength).
270
+ 'glow_map': (GLType.SAMPLER2D, None),
271
+ 'glow_strength': (GLType.FLOAT, 0.0), # 0 disables the whole path
272
+ 'glow_shadow_cut': (GLType.FLOAT, 1.0),
273
+ # Specular rim on the LIT edge of raised surfaces - the edge facing
274
+ # the light source (opposite side from the cast shadow). The shader
275
+ # marches the full-res depth mask toward the light: a lower surface
276
+ # within specular_bevel px marks the silhouette edge, and the pixel
277
+ # is shaded as if the edge were a quarter-round bevel of that
278
+ # radius. light_dir must match ShadowCast's so the highlight always
279
+ # sits opposite the shadow.
280
+ 'light_dir': (GLType.VEC2, (-0.5, 1.6)),
281
+ # Bevel radius in px = width of the highlight rim, the apparent
282
+ # roundness of the edge. 0 disables the pass.
283
+ 'specular_bevel': (GLType.FLOAT, 0.0),
284
+ # Microfacet-style roughness in (0, 1]: low = tight bright rim
285
+ # hugging the edge profile, high = broad dim sheen across the bevel.
286
+ 'specular_roughness': (GLType.FLOAT, 0.4),
287
+ # Peak highlight strength added to the frame (white is).
288
+ 'specular_strength': (GLType.FLOAT, 1.0),
289
+ # Fade of the highlight ALONG perpendicular lit edges, in px: brightness
290
+ # peaks at the owning window's lit corner (where the two lit edges
291
+ # meet) and runs out over this distance scanning away from it. The
292
+ # corner comes from CPU-side window geometry (win_mask/win_rects),
293
+ # interpolated analytically per fragment - perfectly smooth and it
294
+ # moves with the window. 0 = uniform rim.
295
+ 'specular_fade': (GLType.FLOAT, 300.0),
296
+ # Adapts that fade length to the owning window's own size: per axis
297
+ # the fade runs over min(specular_fade, rel * edge_length). Big
298
+ # windows keep the fixed specular_fade look; small windows fade
299
+ # out within their own edge instead of holding a uniform bright
300
+ # rim. 0 disables the adaptation (pure fixed-f fade).
301
+ 'specular_fade_rel': (GLType.FLOAT, 0.6),
302
+ # The frameless OS window's frame (titlebar/etc): its rect sits
303
+ # at frame_origin in the framebuffer, frame_size wide, corners of
304
+ # frame_radius, on a TRANSPARENT background. Inside that rounded rect the
305
+ # composite is the usual darkening; outside it - the shadow margin
306
+ # and the cut corners - there is nothing but the shadow, so the
307
+ # output becomes the shadow itself with premultiplied alpha
308
+ # (shadow_color·s, s). frame_size (0, 0) = no frame, the whole
309
+ # framebuffer is drawn.
310
+ 'frame_origin': (GLType.VEC2, (0.0, 0.0)),
311
+ 'frame_radius': (GLType.FLOAT, 0.0),
312
+ 'frame_size': (GLType.VEC2, (0.0, 0.0)),
313
+ # Window-occlusion mask (blit_offscreen._build_window_mask): R16,
314
+ # each dispatched window's rounded rect stamped back-to-front at
315
+ # rank (i+1)/1024. The specular pass decodes the rank at a
316
+ # fragment to find its owning window...
317
+ 'win_mask': (GLType.SAMPLER2D, None),
318
+ # ...and texelFetches that window's fb-space rect (x0, y0, x1, y1)
319
+ # from this 256x1 RGBA32F table, filled in the same z order.
320
+ 'win_rects': (GLType.SAMPLER2D, None),
321
+ # Depth falloff of the highlight: intensity decays as
322
+ # exp(-receiver_depth * rate), so surfaces near the floor catch
323
+ # the full highlight and high-stacked ones progressively lose it.
324
+ # Depth is in depth_scale'd units (one layer slot ~ 0.3 at the
325
+ # default 64/32 layer/depth ratio). 0 = depth-independent.
326
+ 'specular_depth_falloff': (GLType.FLOAT, 0.0),
327
+ # Minimum depth drop (in depth_scale'd units) that counts as a
328
+ # silhouette edge - rejects same-surface rasterization noise.
329
+ 'specular_depth_eps': (GLType.FLOAT, 0.001),
330
+ # Per-px slope allowance added to that threshold as the march gets
331
+ # farther: a sample only reads as an edge when it sits more than
332
+ # eps + tol*distance below the start. Marks interpolate rank
333
+ # across their quad (per-corner values), so a single background
334
+ # can slope smoothly - this keeps a tilted surface from reading
335
+ # as a phantom edge (only the short bevel march remains, so the
336
+ # allowance rarely exceeds eps in practice).
337
+ 'specular_slope_tol': (GLType.FLOAT, 0.0002),
338
+ 'texture_size': (GLType.VEC2, None),
339
+ }
340
+ fragment_code = """
341
+ // Distance in px from `uv` to the nearest LOWER surface marching along
342
+ // `dir` (a unit, in pixels), up to max_d. Coarse 16-step march to bracket
343
+ // the edge, then a short bisection so long fades don't band into visible
344
+ // steps. Returns max_d when no edge is found in range.
345
+ float _spec_edge_dist(vec2 uv, vec2 dir, float d0, float max_d, vec2 texel) {
346
+ float lo = 0.0;
347
+ float hi = -1.0;
348
+ for (int i = 1; i <= 16; i++) {
349
+ float t = max_d * float(i) / 16.0;
350
+ float d_s = texture(depth_map, uv + dir * t * texel).r * depth_scale;
351
+ // Slope-relative threshold: marks interpolate depth across their
352
+ // quad, so a surface can tilt — only a DROP steeper than the
353
+ // per-px allowance counts as a silhouette edge.
354
+ if (d_s < d0 - specular_depth_eps - specular_slope_tol * t) {
355
+ hi = t; break;
356
+ }
357
+ lo = t;
358
+ }
359
+ if (hi < 0.0) return max_d;
360
+ for (int b = 0; b < 4; b++) {
361
+ float mid = 0.5 * (lo + hi);
362
+ float d_m = texture(depth_map, uv + dir * mid * texel).r * depth_scale;
363
+ if (d_m < d0 - specular_depth_eps - specular_slope_tol * mid) {
364
+ hi = mid;
365
+ } else {
366
+ lo = mid;
367
+ }
368
+ }
369
+ return hi;
370
+ }
371
+
372
+ // Extended sRGB (mirrored for negatives, unbounded above) — hdr_color.py.
373
+ vec3 melty_encode(vec3 v) {
374
+ vec3 a = abs(v);
375
+ vec3 e = mix(a * 12.92, 1.055 * pow(a, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, a));
376
+ return sign(v) * e;
377
+ }
378
+ vec3 melty_decode(vec3 c) {
379
+ vec3 a = abs(c);
380
+ vec3 l = mix(a / 12.92, pow((a + 0.055) / 1.055, vec3(2.4)), step(0.04045, a));
381
+ return sign(c) * l;
382
+ }
383
+
384
+ void main() {
385
+ vec2 uv = v_texcoord;
386
+
387
+ vec4 color = texture(u_texture, uv);
388
+ float depth = texture(depth_map, uv).r * depth_scale;
389
+
390
+ // Joint bilateral upsample of the (possibly low-res) shadow map. A plain
391
+ // bilinear fetch smears the shadow silhouette across the crisp rounded-rect
392
+ // edges, producing visible fringing. Instead we gather the four nearest
393
+ // low-res taps and weight each by (a) the usual bilinear weight and (b) how
394
+ // closely its receiver depth (stored in shadow.a by ShadowCast) matches the
395
+ // full-res depth at this pixel. Taps belonging to a different surface are
396
+ // rejected, so the shadow snaps back to the high-res geometry edge.
397
+ vec2 texel = 1.0 / shadow_size;
398
+ vec2 sample_pos = uv * shadow_size - 0.5;
399
+ vec2 base = floor(sample_pos);
400
+ vec2 frac = sample_pos - base;
401
+
402
+ float shadow_sum = 0.0;
403
+ float weight_sum = 0.0;
404
+
405
+ for (int x = 0; x <= 1; x++) {
406
+ for (int y = 0; y <= 1; y++) {
407
+ vec2 tap = (base + vec2(float(x), float(y)) + 0.5) * texel;
408
+ vec4 s = texture(shadow_map, tap);
409
+
410
+ float wx = (x == 0) ? (1.0 - frac.x) : frac.x;
411
+ float wy = (y == 0) ? (1.0 - frac.y) : frac.y;
412
+ float spatial = wx * wy;
413
+
414
+ float depth_weight = exp(-abs(s.a - depth) * depth_sharpness);
415
+
416
+ float w = spatial * depth_weight;
417
+ shadow_sum += s.r * w;
418
+ weight_sum += w;
419
+ }
420
+ }
421
+
422
+ // Fall back to a straight bilinear fetch if every tap was rejected (e.g. a
423
+ // thin feature with no depth-matching neighbor) so we never punch a hole.
424
+ float shadow_intensity = (weight_sum > 0.0001)
425
+ ? (shadow_sum / weight_sum)
426
+ : texture(shadow_map, uv).r;
427
+
428
+ // Glow rects as light sources: sample the low-res light buffer (bilinear
429
+ // sampler upscales the smooth falloff for free; depth gating already
430
+ // happened per emitter at stamp time). Light eats shadow first, then
431
+ // adds its own emission below.
432
+ vec3 glow_light = vec3(0.0);
433
+ if (glow_strength > 0.0) {
434
+ glow_light = texture(glow_map, uv).rgb;
435
+ float glum = dot(glow_light, vec3(0.299, 0.587, 0.114));
436
+ shadow_intensity *= max(0.0, 1.0 - glum * glow_shadow_cut);
437
+ }
438
+
439
+ // The scene is LINEAR scRGB (hdr_color.py) but every knob here —
440
+ // shadow_color, the opacities, the glow and specular strengths — was
441
+ // tuned against the sRGB-ENCODED frame, so the mix runs in that domain
442
+ // and decodes at the end: the same look, and a shadow_color of
443
+ // (0, 0.02, 0.05) stays the blue-black it reads as (taken as linear
444
+ // light it is sRGB (0, 0.15, 0.25): the blue shadows of 09-07).
445
+ // Extended curves: values above 1 and below 0 pass through.
446
+ vec3 color_enc = melty_encode(color.rgb);
447
+ vec3 shadowed = mix(color_enc, shadow_color, shadow_intensity * shadow_opacity);
448
+ shadowed += melty_encode(glow_light) * glow_strength;
449
+
450
+ // Specular bevel highlight on the lit edge. March toward the light in
451
+ // the full-res depth mask: the first sample that drops below this
452
+ // pixel's surface is the silhouette edge, so this pixel sits on the
453
+ // rim that faces the light (shadows are displaced the OPPOSITE way,
454
+ // toward -light_dir, so highlight and shadow stay consistent). Convex
455
+ // rounded corners fall out for free — the march follows the mask.
456
+ if (specular_bevel > 0.0) {
457
+ vec2 texel_full = 1.0 / texture_size;
458
+ vec2 light_n = normalize(light_dir);
459
+ float edge_t = _spec_edge_dist(uv, light_n, depth,
460
+ specular_bevel, texel_full);
461
+ if (edge_t < specular_bevel) {
462
+ // Quarter-round bevel of radius specular_bevel: at the edge
463
+ // the normal tilts fully toward the light, flattening to
464
+ // straight-up one bevel radius in.
465
+ float f = edge_t / specular_bevel; // 0 edge .. 1 flat
466
+ float sin_t = 1.0 - f;
467
+ float cos_t = sqrt(max(0.0, 1.0 - sin_t * sin_t));
468
+ vec3 n = vec3(light_n * sin_t, cos_t);
469
+ // Light at 45 deg elevation from the light_dir side, viewer
470
+ // straight down; Blinn half-vector with a roughness-driven
471
+ // exponent (rough = broad+dim, smooth = tight+bright).
472
+ vec3 L = normalize(vec3(light_n, 1.0));
473
+ vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
474
+ float rough = clamp(specular_roughness, 0.02, 1.0);
475
+ float shininess = max(1.0, 0.5 / (rough * rough));
476
+ float spec = pow(max(dot(n, H), 0.0), shininess);
477
+ spec *= 1.0 - 0.5 * rough;
478
+
479
+ // Fade along the edge: brightness peaks at the owning
480
+ // window's LIT corner and dies out scanning away from it
481
+ // along either edge. The corner comes from CPU-side window
482
+ // geometry: the win_mask rank at this fragment identifies the
483
+ // topmost window, win_rects holds its fb-space rect, and the
484
+ // fade is an analytic distance field from the rect's corner
485
+ // toward +light_dir. No depth-mask walks — the field is
486
+ // perfectly smooth and translates with the window, so it
487
+ // cannot dash, stair-step, or wobble as views move. Fragments
488
+ // no window owns (floor-level marks) keep fade = 1.
489
+ float fade = 1.0;
490
+ if (specular_fade > 0.0) {
491
+ float zn = texture(win_mask, uv).r;
492
+ if (zn > 0.0) {
493
+ int idx = clamp(int(round(zn * 1024.0)) - 1, 0, 255);
494
+ vec4 r = texelFetch(win_rects, ivec2(idx, 0), 0);
495
+ vec2 frag_px = uv / texel_full;
496
+ vec2 corner = vec2(
497
+ light_n.x > 0.0 ? r.z : r.x,
498
+ light_n.y > 0.0 ? r.w : r.y);
499
+ vec2 d = abs(frag_px - corner);
500
+ vec2 ext = max(r.zw - r.xy, vec2(1.0));
501
+ float u_max = 0.0;
502
+ if (abs(light_n.x) > 1e-3) {
503
+ float fl = (specular_fade_rel > 0.0)
504
+ ? min(specular_fade, specular_fade_rel * ext.x)
505
+ : specular_fade;
506
+ u_max = max(u_max, d.x / max(fl, 1.0));
507
+ }
508
+ if (abs(light_n.y) > 1e-3) {
509
+ float fl = (specular_fade_rel > 0.0)
510
+ ? min(specular_fade, specular_fade_rel * ext.y)
511
+ : specular_fade;
512
+ u_max = max(u_max, d.y / max(fl, 1.0));
513
+ }
514
+ fade = 1.0 - smoothstep(0.0, 1.0, u_max);
515
+ }
516
+ }
517
+ // Depth falloff: low surfaces (near the floor) keep the full
518
+ // highlight, high-stacked ones fade out exponentially.
519
+ if (specular_depth_falloff > 0.0) {
520
+ fade *= exp(-max(depth, 0.0) * specular_depth_falloff);
521
+ }
522
+ shadowed += vec3(spec * fade * specular_strength);
523
+ }
524
+ }
525
+
526
+ // Frameless-window frame: coverage of the content's rounded rect at this
527
+ // pixel (gl_FragCoord: pixel centres, origin bottom-left — the rect is
528
+ // symmetric so no flip). Outside it only the shadow exists: emit it as
529
+ // premultiplied alpha so the compositor blends the desktop through.
530
+ float frame_cov = 1.0;
531
+ if (frame_size.x > 0.0 && frame_size.y > 0.0) {
532
+ vec2 half_size = frame_size * 0.5;
533
+ vec2 fd = abs(gl_FragCoord.xy - frame_origin - half_size) - (half_size - vec2(frame_radius));
534
+ float fdist = length(max(fd, vec2(0.0))) + min(max(fd.x, fd.y), 0.0) - frame_radius;
535
+ frame_cov = 1.0 - smoothstep(-0.5, 0.5, fdist);
536
+ }
537
+ float bg_alpha = shadow_intensity * shadow_opacity;
538
+ vec3 bg_rgb = melty_decode(shadow_color) * bg_alpha;
539
+ fragColor = vec4(mix(bg_rgb, melty_decode(shadowed), frame_cov), mix(bg_alpha, color.a, frame_cov));
540
+ }
541
+ """
542
+
543
+
544
+ # =============================================================================
545
+ # SHADER TYPES
546
+ # =============================================================================
547
+
548
+ @register_shader_type
549
+ class Standard:
550
+ """
551
+ Standard shader type for fullscreen texture processing.
552
+
553
+ Uses a simple quad with position and texture coordinates.
554
+ """
555
+ vertex_in = {
556
+ 'position': GLType.VEC3,
557
+ 'texcoord': GLType.VEC2,
558
+ }
559
+ vertex_out = {
560
+ 'v_texcoord': GLType.VEC2,
561
+ }
562
+ vertex_code = """
563
+ void main() {
564
+ v_texcoord = texcoord;
565
+ gl_Position = vec4(position, 1.0);
566
+ }
567
+ """
568
+
569
+
570
+ @register_shader_type
571
+ class Fullscreen:
572
+ """
573
+ Fullscreen shader type - generates texture coordinates from position.
574
+
575
+ Useful when you don't have explicit texture coordinates.
576
+ """
577
+ vertex_in = {
578
+ 'position': GLType.VEC3,
579
+ }
580
+ vertex_out = {
581
+ 'v_texcoord': GLType.VEC2,
582
+ }
583
+ vertex_code = """
584
+ void main() {
585
+ v_texcoord = position.xy * 0.5 + 0.5;
586
+ gl_Position = vec4(position, 1.0);
587
+ }
588
+ """
589
+
590
+
591
+
592
+ @register_shader_type
593
+ class Transformed:
594
+ """
595
+ Shader type with model-view-projection transformation.
596
+
597
+ Useful for 3D scenes or when you need transforms.
598
+ """
599
+ vertex_in = {
600
+ 'position': GLType.VEC3,
601
+ 'texcoord': GLType.VEC2,
602
+ }
603
+ vertex_out = {
604
+ 'v_texcoord': GLType.VEC2,
605
+ }
606
+ vertex_code = """
607
+ void main() {
608
+ v_texcoord = texcoord;
609
+ gl_Position = u_projection * u_modelview * vec4(position, 1.0);
610
+ }
611
+ """
612
+
613
+
614
+ # =============================================================================
615
+ # BASIC FILTERS
616
+ # =============================================================================
617
+
618
+ @register_shader
619
+ class Jet:
620
+ """
621
+ Apply jet colormap to the red channel of a texture.
622
+
623
+ Maps input red values [0, 1] to the jet color scale
624
+ (blue -> cyan -> green -> yellow -> red).
625
+
626
+ Input uniforms:
627
+ u_texture: Input texture (uses red channel)
628
+
629
+ Output: RGB jet-colored representation of input intensity
630
+ """
631
+ shader_type = 'standard'
632
+ uniforms = {
633
+ 'offset': (GLType.FLOAT, 0.0), # -1.0 to 1.0
634
+ }
635
+ fragment_code = """
636
+ vec3 jet(float t) {
637
+ // Clamp input to [0, 1]
638
+ t += offset;
639
+ t = clamp(t, 0.0, 1.0);
640
+
641
+ // Compute RGB components using piecewise linear approximation
642
+ // Each channel is a trapezoid function centered at different positions
643
+ float r = clamp(1.5 - abs(t - 0.75) * 4.0, 0.0, 1.0);
644
+ float g = clamp(1.5 - abs(t - 0.5) * 4.0, 0.0, 1.0);
645
+ float b = clamp(1.5 - abs(t - 0.25) * 4.0, 0.0, 1.0);
646
+
647
+ return vec3(r, g, b);
648
+ }
649
+
650
+ void main() {
651
+ float intensity = texture(u_texture, v_texcoord).r;
652
+ fragColor = vec4(jet(intensity), 1.0);
653
+ }
654
+ """
655
+
656
+ @register_shader
657
+ class Passthrough:
658
+ """Simple passthrough - copies input to output unchanged."""
659
+ shader_type = 'standard'
660
+ uniforms = {}
661
+ fragment_code = """
662
+ void main() {
663
+ fragColor = texture(u_texture, v_texcoord);
664
+ }
665
+ """
666
+
667
+
668
+ @register_shader
669
+ class BrightnessContrast:
670
+ """Adjust brightness and contrast of an image."""
671
+ shader_type = 'standard'
672
+ uniforms = {
673
+ 'brightness': (GLType.FLOAT, 0.0), # -1.0 to 1.0
674
+ 'contrast': (GLType.FLOAT, 1.0), # 0.0 to 2.0+
675
+ }
676
+ fragment_code = """
677
+ void main() {
678
+ vec4 color = texture(u_texture, v_texcoord);
679
+ // Apply contrast around midpoint, then add brightness
680
+ color.rgb = (color.rgb - 0.5) * contrast + 0.5 + brightness;
681
+ // No ceiling and no floor: the working space is linear scRGB
682
+ // (hdr_color.py) -- values above 1.0 are real highlights and NEGATIVE
683
+ // components are colours outside sRGB (a BT.2020 green is
684
+ // (-1.9, 3.6, -0.3)). Flooring them here collapsed every wide-gamut
685
+ // pixel to the sRGB gamut before the present pass could convert
686
+ // primaries, so HDR screenshots showed muted (09-10). The present
687
+ // pass clamps AFTER its primaries matrix, where it is safe.
688
+ fragColor = color;
689
+ }
690
+ """
691
+
692
+
693
+ @register_shader
694
+ class HueSaturation:
695
+ """Adjust hue, saturation, and lightness."""
696
+ shader_type = 'standard'
697
+ uniforms = {
698
+ 'hue_shift': (GLType.FLOAT, 0.0), # -1.0 to 1.0 (maps to -180 to 180 degrees)
699
+ 'saturation': (GLType.FLOAT, 1.0), # 0.0 to 2.0+
700
+ 'lightness': (GLType.FLOAT, 0.0), # -1.0 to 1.0
701
+ }
702
+ fragment_code = """
703
+ vec3 rgb2hsl(vec3 c) {
704
+ float maxc = max(max(c.r, c.g), c.b);
705
+ float minc = min(min(c.r, c.g), c.b);
706
+ float l = (maxc + minc) / 2.0;
707
+
708
+ if (maxc == minc) {
709
+ return vec3(0.0, 0.0, l);
710
+ }
711
+
712
+ float d = maxc - minc;
713
+ float s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
714
+
715
+ float h;
716
+ if (maxc == c.r) {
717
+ h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
718
+ } else if (maxc == c.g) {
719
+ h = (c.b - c.r) / d + 2.0;
720
+ } else {
721
+ h = (c.r - c.g) / d + 4.0;
722
+ }
723
+ h /= 6.0;
724
+
725
+ return vec3(h, s, l);
726
+ }
727
+
728
+ float hue2rgb(float p, float q, float t) {
729
+ if (t < 0.0) t += 1.0;
730
+ if (t > 1.0) t -= 1.0;
731
+ if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
732
+ if (t < 1.0/2.0) return q;
733
+ if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
734
+ return p;
735
+ }
736
+
737
+ vec3 hsl2rgb(vec3 c) {
738
+ if (c.y == 0.0) {
739
+ return vec3(c.z);
740
+ }
741
+
742
+ float q = c.z < 0.5 ? c.z * (1.0 + c.y) : c.z + c.y - c.z * c.y;
743
+ float p = 2.0 * c.z - q;
744
+
745
+ return vec3(
746
+ hue2rgb(p, q, c.x + 1.0/3.0),
747
+ hue2rgb(p, q, c.x),
748
+ hue2rgb(p, q, c.x - 1.0/3.0)
749
+ );
750
+ }
751
+
752
+ // sRGB <-> BT.2020 primaries (linear, D65; column-major, same numbers as
753
+ // hdr_color.MELTY_SRGB_TO_BT2020). HSL is done in BT.2020 because it
754
+ // contains everything a panel shows (P3 included): a wide colour is scRGB
755
+ // with NEGATIVE components (hdr_color.py) and HSL cannot take those. The
756
+ // old max(rgb, 0) collapsed such pixels to the sRGB gamut, so HDR
757
+ // screenshots showed muted even at saturation 1 (09-10).
758
+ const mat3 SRGB_TO_BT2020 = mat3(
759
+ 0.6274039, 0.0690973, 0.0163914,
760
+ 0.3292830, 0.9195404, 0.0880133,
761
+ 0.0433131, 0.0113623, 0.8955953);
762
+ const mat3 BT2020_TO_SRGB = mat3(
763
+ 1.6604910, -0.1245505, -0.0181508,
764
+ -0.5876411, 1.1328999, -0.1005789,
765
+ -0.0728499, -0.0083494, 1.1187297);
766
+
767
+ void main() {
768
+ vec4 color = texture(u_texture, v_texcoord);
769
+ if (hue_shift == 0.0 && saturation == 1.0 && lightness == 0.0) {
770
+ fragColor = color; // identity: bit-exact passthrough
771
+ return;
772
+ }
773
+ vec3 wide = max(SRGB_TO_BT2020 * color.rgb, 0.0);
774
+ // HSL is defined on [0, 1]; an HDR pixel (a component above 1.0) is
775
+ // normalised by its brightest component first and scaled back after,
776
+ // so highlights keep their level through the edit.
777
+ float scale = max(max(max(wide.r, wide.g), wide.b), 1.0);
778
+ vec3 hsl = rgb2hsl(wide / scale);
779
+
780
+ // Apply adjustments
781
+ hsl.x = fract(hsl.x + hue_shift * 0.5); // Hue shift
782
+ hsl.y = clamp(hsl.y * saturation, 0.0, 1.0); // Saturation
783
+ hsl.z = clamp(hsl.z + lightness, 0.0, 1.0); // Lightness
784
+
785
+ fragColor = vec4(BT2020_TO_SRGB * (hsl2rgb(hsl) * scale), color.a);
786
+ }
787
+ """
788
+
789
+
790
+ @register_shader
791
+ class Grayscale:
792
+ """Convert to grayscale with adjustable method."""
793
+ shader_type = 'standard'
794
+ uniforms = {
795
+ 'method': (GLType.INT, 0), # 0=luminance, 1=average, 2=lightness
796
+ }
797
+ fragment_code = """
798
+ void main() {
799
+ vec4 color = texture(u_texture, v_texcoord);
800
+ float gray;
801
+
802
+ if (method == 0) {
803
+ // Luminance (perceptual)
804
+ gray = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
805
+ } else if (method == 1) {
806
+ // Average
807
+ gray = (color.r + color.g + color.b) / 3.0;
808
+ } else {
809
+ // Lightness
810
+ gray = (max(max(color.r, color.g), color.b) + min(min(color.r, color.g), color.b)) / 2.0;
811
+ }
812
+
813
+ fragColor = vec4(vec3(gray), color.a);
814
+ }
815
+ """
816
+
817
+
818
+ @register_shader
819
+ class Invert:
820
+ """Invert colors."""
821
+ shader_type = 'standard'
822
+ uniforms = {
823
+ 'amount': (GLType.FLOAT, 1.0), # 0.0 to 1.0
824
+ }
825
+ fragment_code = """
826
+ void main() {
827
+ vec4 color = texture(u_texture, v_texcoord);
828
+ vec3 inverted = 1.0 - color.rgb;
829
+ fragColor = vec4(mix(color.rgb, inverted, amount), color.a);
830
+ }
831
+ """
832
+
833
+
834
+ @register_shader
835
+ class Gamma:
836
+ """Apply gamma correction."""
837
+ shader_type = 'standard'
838
+ uniforms = {
839
+ 'gamma': (GLType.FLOAT, 1.0), # 0.1 to 3.0+
840
+ }
841
+ fragment_code = """
842
+ void main() {
843
+ vec4 color = texture(u_texture, v_texcoord);
844
+ color.rgb = pow(color.rgb, vec3(1.0 / gamma));
845
+ fragColor = color;
846
+ }
847
+ """
848
+
849
+
850
+ @register_shader
851
+ class Threshold:
852
+ """Apply threshold to create binary image."""
853
+ shader_type = 'standard'
854
+ uniforms = {
855
+ 'threshold': (GLType.FLOAT, 0.5), # 0.0 to 1.0
856
+ 'smoothness': (GLType.FLOAT, 0.0), # 0.0 = hard, higher = softer
857
+ }
858
+ fragment_code = """
859
+ void main() {
860
+ vec4 color = texture(u_texture, v_texcoord);
861
+ float lum = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
862
+ float value = smoothstep(threshold - smoothness, threshold + smoothness, lum);
863
+ fragColor = vec4(vec3(value), color.a);
864
+ }
865
+ """
866
+
867
+
868
+ # =============================================================================
869
+ # BLUR FILTERS
870
+ # =============================================================================
871
+
872
+ @register_shader
873
+ class BoxBlur:
874
+ """Simple box blur (uniform weighted average)."""
875
+ shader_type = 'standard'
876
+ uniforms = {
877
+ 'radius': (GLType.FLOAT, 1.0),
878
+ 'texture_size': (GLType.VEC2, (512.0, 512.0)),
879
+ }
880
+ fragment_code = """
881
+ void main() {
882
+ vec2 texel = 1.0 / texture_size;
883
+ vec4 sum = vec4(0.0);
884
+ int r = int(radius);
885
+ float count = 0.0;
886
+
887
+ for (int x = -r; x <= r; x++) {
888
+ for (int y = -r; y <= r; y++) {
889
+ sum += texture(u_texture, v_texcoord + vec2(float(x), float(y)) * texel);
890
+ count += 1.0;
891
+ }
892
+ }
893
+
894
+ fragColor = sum / count;
895
+ }
896
+ """
897
+
898
+
899
+ @register_shader
900
+ class GaussianBlurH:
901
+ """Horizontal pass of separable Gaussian blur."""
902
+ shader_type = 'standard'
903
+ uniforms = {
904
+ 'radius': (GLType.FLOAT, 4.0),
905
+ 'texture_size': (GLType.VEC2, (512.0, 512.0)),
906
+ }
907
+ fragment_code = """
908
+ void main() {
909
+ vec2 texel = vec2(1.0 / texture_size.x, 0.0);
910
+
911
+ // Pre-computed Gaussian weights for sigma = radius/3
912
+ float weights[9];
913
+ weights[0] = 0.0625; weights[1] = 0.125; weights[2] = 0.1875;
914
+ weights[3] = 0.25; weights[4] = 0.1875; weights[5] = 0.125;
915
+ weights[6] = 0.0625; weights[7] = 0.0; weights[8] = 0.0;
916
+
917
+ vec4 sum = vec4(0.0);
918
+ float total = 0.0;
919
+ int r = int(radius);
920
+
921
+ for (int i = -r; i <= r; i++) {
922
+ float w = exp(-float(i*i) / (2.0 * radius * radius / 9.0));
923
+ sum += texture(u_texture, v_texcoord + float(i) * texel) * w;
924
+ total += w;
925
+ }
926
+
927
+ fragColor = sum / total;
928
+ }
929
+ """
930
+
931
+
932
+ @register_shader
933
+ class GaussianBlurV:
934
+ """Vertical pass of separable Gaussian blur."""
935
+ shader_type = 'standard'
936
+ uniforms = {
937
+ 'radius': (GLType.FLOAT, 4.0),
938
+ 'texture_size': (GLType.VEC2, (512.0, 512.0)),
939
+ }
940
+ fragment_code = """
941
+ void main() {
942
+ vec2 texel = vec2(0.0, 1.0 / texture_size.y);
943
+
944
+ vec4 sum = vec4(0.0);
945
+ float total = 0.0;
946
+ int r = int(radius);
947
+
948
+ for (int i = -r; i <= r; i++) {
949
+ float w = exp(-float(i*i) / (2.0 * radius * radius / 9.0));
950
+ sum += texture(u_texture, v_texcoord + float(i) * texel) * w;
951
+ total += w;
952
+ }
953
+
954
+ fragColor = sum / total;
955
+ }
956
+ """
957
+
958
+
959
+ @register_shader
960
+ class GaussianBlur:
961
+ """Combined Gaussian blur (both horizontal and vertical in one pass)."""
962
+ shader_type = 'standard'
963
+ uniforms = {
964
+ 'radius': (GLType.FLOAT, 4.0),
965
+ 'texture_size': (GLType.VEC2, None),
966
+ }
967
+ fragment_code = """
968
+ void main() {
969
+ vec2 texel = 1.0 / texture_size;
970
+
971
+ vec4 sum = vec4(0.0);
972
+ float total = 0.0;
973
+ int r = int(radius);
974
+
975
+ // 2D Gaussian blur - sample in both directions
976
+ for (int x = -r; x <= r; x++) {
977
+ for (int y = -r; y <= r; y++) {
978
+ float dist = float(x*x + y*y);
979
+ float w = exp(-dist / (2.0 * radius * radius / 9.0));
980
+ sum += texture(u_texture, v_texcoord + vec2(float(x), float(y)) * texel) * w;
981
+ total += w;
982
+ }
983
+ }
984
+
985
+ fragColor = sum / total;
986
+ }
987
+ """
988
+
989
+
990
+ # =============================================================================
991
+ # EDGE DETECTION
992
+ # =============================================================================
993
+
994
+ @register_shader
995
+ class Sobel:
996
+ """Sobel edge detection."""
997
+ shader_type = 'standard'
998
+ uniforms = {
999
+ 'texture_size': (GLType.VEC2, (512.0, 512.0)),
1000
+ 'strength': (GLType.FLOAT, 1.0),
1001
+ }
1002
+ fragment_code = """
1003
+ void main() {
1004
+ vec2 texel = 1.0 / texture_size;
1005
+
1006
+ // Sample 3x3 neighborhood
1007
+ float tl = dot(texture(u_texture, v_texcoord + vec2(-1, -1) * texel).rgb, vec3(0.299, 0.587, 0.114));
1008
+ float t = dot(texture(u_texture, v_texcoord + vec2( 0, -1) * texel).rgb, vec3(0.299, 0.587, 0.114));
1009
+ float tr = dot(texture(u_texture, v_texcoord + vec2( 1, -1) * texel).rgb, vec3(0.299, 0.587, 0.114));
1010
+ float l = dot(texture(u_texture, v_texcoord + vec2(-1, 0) * texel).rgb, vec3(0.299, 0.587, 0.114));
1011
+ float r = dot(texture(u_texture, v_texcoord + vec2( 1, 0) * texel).rgb, vec3(0.299, 0.587, 0.114));
1012
+ float bl = dot(texture(u_texture, v_texcoord + vec2(-1, 1) * texel).rgb, vec3(0.299, 0.587, 0.114));
1013
+ float b = dot(texture(u_texture, v_texcoord + vec2( 0, 1) * texel).rgb, vec3(0.299, 0.587, 0.114));
1014
+ float br = dot(texture(u_texture, v_texcoord + vec2( 1, 1) * texel).rgb, vec3(0.299, 0.587, 0.114));
1015
+
1016
+ // Sobel kernels
1017
+ float gx = -tl - 2.0*l - bl + tr + 2.0*r + br;
1018
+ float gy = -tl - 2.0*t - tr + bl + 2.0*b + br;
1019
+
1020
+ float edge = sqrt(gx*gx + gy*gy) * strength;
1021
+ fragColor = vec4(vec3(edge), 1.0);
1022
+ }
1023
+ """
1024
+
1025
+
1026
+ @register_shader
1027
+ class Laplacian:
1028
+ """Laplacian edge detection."""
1029
+ shader_type = 'standard'
1030
+ uniforms = {
1031
+ 'texture_size': (GLType.VEC2, (512.0, 512.0)),
1032
+ 'strength': (GLType.FLOAT, 1.0),
1033
+ }
1034
+ fragment_code = """
1035
+ void main() {
1036
+ vec2 texel = 1.0 / texture_size;
1037
+
1038
+ vec4 center = texture(u_texture, v_texcoord);
1039
+ vec4 t = texture(u_texture, v_texcoord + vec2(0, -1) * texel);
1040
+ vec4 b = texture(u_texture, v_texcoord + vec2(0, 1) * texel);
1041
+ vec4 l = texture(u_texture, v_texcoord + vec2(-1, 0) * texel);
1042
+ vec4 r = texture(u_texture, v_texcoord + vec2( 1, 0) * texel);
1043
+
1044
+ vec4 edge = (t + b + l + r - 4.0 * center) * strength;
1045
+ fragColor = vec4(abs(edge.rgb), center.a);
1046
+ }
1047
+ """
1048
+
1049
+
1050
+ # =============================================================================
1051
+ # COLOR EFFECTS
1052
+ # =============================================================================
1053
+
1054
+ @register_shader
1055
+ class Sepia:
1056
+ """Apply sepia tone effect."""
1057
+ shader_type = 'standard'
1058
+ uniforms = {
1059
+ 'amount': (GLType.FLOAT, 1.0),
1060
+ }
1061
+ fragment_code = """
1062
+ void main() {
1063
+ vec4 color = texture(u_texture, v_texcoord);
1064
+
1065
+ vec3 sepia;
1066
+ sepia.r = dot(color.rgb, vec3(0.393, 0.769, 0.189));
1067
+ sepia.g = dot(color.rgb, vec3(0.349, 0.686, 0.168));
1068
+ sepia.b = dot(color.rgb, vec3(0.272, 0.534, 0.131));
1069
+
1070
+ fragColor = vec4(mix(color.rgb, sepia, amount), color.a);
1071
+ }
1072
+ """
1073
+
1074
+
1075
+ @register_shader
1076
+ class Vignette:
1077
+ """Apply vignette effect (darkened edges)."""
1078
+ shader_type = 'standard'
1079
+ uniforms = {
1080
+ 'radius': (GLType.FLOAT, 0.75), # Inner radius where vignette starts
1081
+ 'softness': (GLType.FLOAT, 0.45), # Softness of the vignette
1082
+ 'amount': (GLType.FLOAT, 0.5), # Darkness amount
1083
+ }
1084
+ fragment_code = """
1085
+ void main() {
1086
+ vec4 color = texture(u_texture, v_texcoord);
1087
+
1088
+ vec2 center = v_texcoord - 0.5;
1089
+ float dist = length(center) * 1.414; // Normalize to corners = 1
1090
+
1091
+ float vignette = smoothstep(radius, radius - softness, dist);
1092
+ color.rgb = mix(color.rgb * (1.0 - amount), color.rgb, vignette);
1093
+
1094
+ fragColor = color;
1095
+ }
1096
+ """
1097
+
1098
+
1099
+ @register_shader
1100
+ class ChromaticAberration:
1101
+ """Simulate chromatic aberration (color fringing)."""
1102
+ shader_type = 'standard'
1103
+ uniforms = {
1104
+ 'amount': (GLType.FLOAT, 0.005),
1105
+ }
1106
+ fragment_code = """
1107
+ void main() {
1108
+ vec2 direction = v_texcoord - 0.5;
1109
+
1110
+ float r = texture(u_texture, v_texcoord + direction * amount).r;
1111
+ float g = texture(u_texture, v_texcoord).g;
1112
+ float b = texture(u_texture, v_texcoord - direction * amount).b;
1113
+ float a = texture(u_texture, v_texcoord).a;
1114
+
1115
+ fragColor = vec4(r, g, b, a);
1116
+ }
1117
+ """
1118
+
1119
+
1120
+ @register_shader
1121
+ class Posterize:
1122
+ """Reduce color levels for a poster effect."""
1123
+ shader_type = 'standard'
1124
+ uniforms = {
1125
+ 'levels': (GLType.FLOAT, 8.0), # Number of levels per channel
1126
+ }
1127
+ fragment_code = """
1128
+ void main() {
1129
+ vec4 color = texture(u_texture, v_texcoord);
1130
+ color.rgb = floor(color.rgb * levels + 0.5) / levels;
1131
+ fragColor = color;
1132
+ }
1133
+ """
1134
+
1135
+
1136
+ # =============================================================================
1137
+ # DISTORTION
1138
+ # =============================================================================
1139
+
1140
+ @register_shader
1141
+ class Pixelate:
1142
+ """Pixelate effect."""
1143
+ shader_type = 'standard'
1144
+ uniforms = {
1145
+ 'pixel_size': (GLType.FLOAT, 8.0),
1146
+ 'texture_size': (GLType.VEC2, (512.0, 512.0)),
1147
+ }
1148
+ fragment_code = """
1149
+ void main() {
1150
+ vec2 size = pixel_size / texture_size;
1151
+ vec2 coord = floor(v_texcoord / size) * size + size * 0.5;
1152
+ fragColor = texture(u_texture, coord);
1153
+ }
1154
+ """
1155
+
1156
+
1157
+ @register_shader
1158
+ class Swirl:
1159
+ """Swirl distortion effect."""
1160
+ shader_type = 'standard'
1161
+ uniforms = {
1162
+ 'angle': (GLType.FLOAT, 3.0), # Swirl angle
1163
+ 'radius': (GLType.FLOAT, 0.5), # Effect radius
1164
+ 'center': (GLType.VEC2, (0.5, 0.5)),
1165
+ }
1166
+ fragment_code = """
1167
+ void main() {
1168
+ vec2 coord = v_texcoord - center;
1169
+ float dist = length(coord);
1170
+
1171
+ if (dist < radius) {
1172
+ float percent = (radius - dist) / radius;
1173
+ float theta = percent * percent * angle;
1174
+ float s = sin(theta);
1175
+ float c = cos(theta);
1176
+ coord = vec2(
1177
+ coord.x * c - coord.y * s,
1178
+ coord.x * s + coord.y * c
1179
+ );
1180
+ }
1181
+
1182
+ fragColor = texture(u_texture, coord + center);
1183
+ }
1184
+ """
1185
+
1186
+
1187
+ @register_shader
1188
+ class Bulge:
1189
+ """Bulge/pinch distortion effect."""
1190
+ shader_type = 'standard'
1191
+ uniforms = {
1192
+ 'strength': (GLType.FLOAT, 0.5), # Positive = bulge, negative = pinch
1193
+ 'radius': (GLType.FLOAT, 0.5),
1194
+ 'center': (GLType.VEC2, (0.5, 0.5)),
1195
+ }
1196
+ fragment_code = """
1197
+ void main() {
1198
+ vec2 coord = v_texcoord - center;
1199
+ float dist = length(coord);
1200
+
1201
+ if (dist < radius) {
1202
+ float percent = dist / radius;
1203
+ float distortion = pow(percent, 1.0 - strength);
1204
+ coord = coord * distortion;
1205
+ }
1206
+
1207
+ fragColor = texture(u_texture, coord + center);
1208
+ }
1209
+ """
1210
+
1211
+
1212
+ # =============================================================================
1213
+ # NORMALIZATION
1214
+ # =============================================================================
1215
+
1216
+ @register_shader
1217
+ class MinMaxReduction:
1218
+ """
1219
+ Reduction shader for finding min/max values in a texture.
1220
+
1221
+ Each output pixel represents the min/max of a 2x2 block from the input.
1222
+ This is used internally by the normalize() method for parallel reduction.
1223
+
1224
+ Output format: R=min_value, G=max_value (across all RGB channels)
1225
+ """
1226
+ shader_type = 'standard'
1227
+ uniforms = {
1228
+ 'texture_size': (GLType.VEC2, None),
1229
+ }
1230
+ fragment_code = """
1231
+ void main() {
1232
+ vec2 texel = 1.0 / texture_size;
1233
+
1234
+ // Sample 2x2 block
1235
+ vec3 s00 = texture(u_texture, v_texcoord + vec2(0.0, 0.0) * texel).rgb;
1236
+ vec3 s10 = texture(u_texture, v_texcoord + vec2(1.0, 0.0) * texel).rgb;
1237
+ vec3 s01 = texture(u_texture, v_texcoord + vec2(0.0, 1.0) * texel).rgb;
1238
+ vec3 s11 = texture(u_texture, v_texcoord + vec2(1.0, 1.0) * texel).rgb;
1239
+
1240
+ // Find min and max across all channels in the 2x2 block
1241
+ float min_val = min(min(min(s00.r, s00.g), min(s00.b, s10.r)),
1242
+ min(min(s10.g, s10.b), min(s01.r, s01.g)));
1243
+ min_val = min(min_val, min(min(s01.b, s11.r), min(s11.g, s11.b)));
1244
+
1245
+ float max_val = max(max(max(s00.r, s00.g), max(s00.b, s10.r)),
1246
+ max(max(s10.g, s10.b), max(s01.r, s01.g)));
1247
+ max_val = max(max_val, max(max(s01.b, s11.r), max(s11.g, s11.b)));
1248
+
1249
+ // Store min in R, max in G
1250
+ fragColor = vec4(min_val, max_val, 0.0, 1.0);
1251
+ }
1252
+ """
1253
+
1254
+
1255
+ @register_shader
1256
+ class NormalizeRemap:
1257
+ """
1258
+ Remaps texture values from [min, max] to [0, 1].
1259
+
1260
+ Used internally by the normalize() method after min/max calculation.
1261
+ """
1262
+ shader_type = 'standard'
1263
+ uniforms = {
1264
+ # (min, max) live in the .r/.g of a 1x1 texture stored on the GPU by
1265
+ # texture_min_max - sampled here so the range is read straight from the
1266
+ # GPU with no CPU readback.
1267
+ 'min_max_tex': (GLType.SAMPLER2D, None),
1268
+ }
1269
+ fragment_code = """
1270
+ void main() {
1271
+ vec4 color = texture(u_texture, v_texcoord);
1272
+
1273
+ vec2 mm = texelFetch(min_max_tex, ivec2(0, 0), 0).rg;
1274
+ float min_value = mm.x;
1275
+ float max_value = mm.y;
1276
+
1277
+ // Avoid division by zero
1278
+ float range = max_value - min_value;
1279
+ if (abs(range) < 1e-20) range = 1.0;
1280
+
1281
+ // Remap from [min, max] to [0, 1]
1282
+ color.rgb = (color.rgb - min_value) / range;
1283
+
1284
+ fragColor = color;
1285
+ }
1286
+ """
1287
+
1288
+
1289
+ @register_shader
1290
+ class Multiply:
1291
+ """Scale every colour channel by `factor` (alpha untouched): a plain
1292
+ exposure change, the dim behind draw_texture's crop selection."""
1293
+ shader_type = 'standard'
1294
+ uniforms = {
1295
+ 'factor': (GLType.FLOAT, 1.0),
1296
+ }
1297
+ fragment_code = """
1298
+ void main() {
1299
+ vec4 color = texture(u_texture, v_texcoord);
1300
+ fragColor = vec4(color.rgb * factor, color.a);
1301
+ }
1302
+ """