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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
meltygui/hdr_color.py
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
"""Melty colour: extended-sRGB tuples in Python, HDR/P3 through imgui's u32.
|
|
2
|
+
|
|
3
|
+
The convention every colour in Melty follows:
|
|
4
|
+
|
|
5
|
+
* A colour TUPLE ``(r, g, b[, a])`` is EXTENDED sRGB: the sRGB transfer
|
|
6
|
+
curve, mirrored for negatives, with no ceiling. ``1.0`` is the desktop's
|
|
7
|
+
SDR reference white (what an untagged sRGB window shows as white), values
|
|
8
|
+
above it are brighter, negative components are colours outside the sRGB
|
|
9
|
+
gamut (a P3 red is ``(1.093, -0.227, -0.150)``, the same numbers CSS gives
|
|
10
|
+
for ``color(display-p3 1 0 0)`` in ``srgb``). Every existing colour stays
|
|
11
|
+
valid, and the helpers ``white(n)`` / ``p3(r, g, b)`` build the rest.
|
|
12
|
+
|
|
13
|
+
* The working space on the GPU is LINEAR scRGB: sRGB primaries, linear
|
|
14
|
+
light, fp16, negatives allowed. That is what the compositor (Hyprland's
|
|
15
|
+
``ext_linear`` surfaces) composites natively.
|
|
16
|
+
|
|
17
|
+
* Between the two sits imgui's 32-bit vertex colour, the one narrow pipe.
|
|
18
|
+
``pack_color`` encodes into it and the vertex shader (``GLSL_DECODE``)
|
|
19
|
+
decodes out of it; the two are the only code that knows the layout:
|
|
20
|
+
|
|
21
|
+
bit 31 : SDR bit — 1 = the RGB bytes are plain 8-bit sRGB in [0, 1]
|
|
22
|
+
(byte-identical to imgui's own packing, so an opaque
|
|
23
|
+
colour imgui converts itself — ``imgui.image``'s white
|
|
24
|
+
tint, ``0xFFFFFFFF`` — reads correctly), 0 = HDR: the
|
|
25
|
+
RGB bytes are DISPLAY P3 primaries on a log curve over
|
|
26
|
+
[0, Toggles.HDR.vertex_range] (both headroom AND gamut
|
|
27
|
+
ride the one flag; P3 contains sRGB so anything the
|
|
28
|
+
panel can show packs non-negative)
|
|
29
|
+
bits 24..30 : 7-bit alpha (128 levels, both modes)
|
|
30
|
+
bits 0..23 : R, G, B bytes (little-endian, R lowest — imgui's order)
|
|
31
|
+
|
|
32
|
+
imgui itself only ever touches the alpha BYTE of a packed colour (the
|
|
33
|
+
style-alpha multiply, the alpha-zero skip), never the RGB bytes, which is
|
|
34
|
+
what makes re-purposing them safe. An SDR colour keeps every one of its
|
|
35
|
+
256 codes per channel; an HDR colour gets its own 256 codes over the
|
|
36
|
+
extended range instead of sharing them.
|
|
37
|
+
|
|
38
|
+
Style-table entries go through ``style_color`` so imgui's own float→u32
|
|
39
|
+
conversion reproduces these bytes exactly.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import math
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# The curve (extended sRGB) and primaries
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def srgb_to_linear(c: float) -> float:
|
|
51
|
+
"""Extended sRGB decode: the sRGB curve, mirrored for negatives, no ceiling."""
|
|
52
|
+
s = -1.0 if c < 0.0 else 1.0
|
|
53
|
+
c = abs(c)
|
|
54
|
+
if c <= 0.04045:
|
|
55
|
+
return s * c / 12.92
|
|
56
|
+
return s * ((c + 0.055) / 1.055) ** 2.4
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def linear_to_srgb(v: float) -> float:
|
|
60
|
+
"""Extended sRGB encode: inverse of ``srgb_to_linear``."""
|
|
61
|
+
s = -1.0 if v < 0.0 else 1.0
|
|
62
|
+
v = abs(v)
|
|
63
|
+
if v <= 0.0031308:
|
|
64
|
+
return s * v * 12.92
|
|
65
|
+
return s * (1.055 * v ** (1.0 / 2.4) - 0.055)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _mat_mul_vec(m, v):
|
|
69
|
+
return tuple(m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2] for i in range(3))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _mat_mul(a, b):
|
|
73
|
+
return tuple(tuple(sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)) for i in range(3))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _mat_inv(m):
|
|
77
|
+
(a, b, c), (d, e, f), (g, h, i) = m
|
|
78
|
+
det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
|
|
79
|
+
return (
|
|
80
|
+
((e * i - f * h) / det, (c * h - b * i) / det, (b * f - c * e) / det),
|
|
81
|
+
((f * g - d * i) / det, (a * i - c * g) / det, (c * d - a * f) / det),
|
|
82
|
+
((d * h - e * g) / det, (b * g - a * h) / det, (a * e - b * d) / det),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# RGB → XYZ (D65) for the two primaries sets. P3 here is DISPLAY P3 (the
|
|
87
|
+
# sRGB curve on DCI-P3 primaries with a D65 white), what the panel and CSS mean.
|
|
88
|
+
_SRGB_TO_XYZ = ((0.4123908, 0.3575843, 0.1804808),
|
|
89
|
+
(0.2126390, 0.7151687, 0.0721923),
|
|
90
|
+
(0.0193308, 0.1191948, 0.9505322))
|
|
91
|
+
_P3_TO_XYZ = ((0.4865709, 0.2656677, 0.1982173),
|
|
92
|
+
(0.2289746, 0.6917385, 0.0792869),
|
|
93
|
+
(0.0000000, 0.0451134, 1.0439444))
|
|
94
|
+
|
|
95
|
+
SRGB_TO_P3 = _mat_mul(_mat_inv(_P3_TO_XYZ), _SRGB_TO_XYZ)
|
|
96
|
+
P3_TO_SRGB = _mat_mul(_mat_inv(_SRGB_TO_XYZ), _P3_TO_XYZ)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def linear_srgb_to_p3(rgb):
|
|
100
|
+
return _mat_mul_vec(SRGB_TO_P3, rgb)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def linear_p3_to_srgb(rgb):
|
|
104
|
+
return _mat_mul_vec(P3_TO_SRGB, rgb)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Oklab: the perceptual space HDR colour SCALES are authored in
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Björn Ottosson's Oklab on LINEAR sRGB light. It is scale-invariant (no
|
|
111
|
+
# absolute nits: lightness is a cube root, so 16 × white sits at L = 2.52)
|
|
112
|
+
# and works on extended values (negative components = P3), which is what a
|
|
113
|
+
# ramp that keeps climbing past the desktop's white needs. Its cube-root
|
|
114
|
+
# lightness is a fair match for equal perceptual steps above white; it does
|
|
115
|
+
# NOT model the Hunt effect (bright colours reading more saturated), so a
|
|
116
|
+
# uniform-L ramp is "uniform" in the CIELAB sense, not a full HDR appearance
|
|
117
|
+
# model.
|
|
118
|
+
|
|
119
|
+
_OKLAB_M1 = ((0.4122214708, 0.5363325363, 0.0514459929),
|
|
120
|
+
(0.2119034982, 0.6806995451, 0.1073969566),
|
|
121
|
+
(0.0883024619, 0.2817188376, 0.6299787005))
|
|
122
|
+
_OKLAB_M2 = ((0.2104542553, 0.7936177850, -0.0040720468),
|
|
123
|
+
(1.9779984951, -2.4285922050, 0.4505937099),
|
|
124
|
+
(0.0259040371, 0.7827717662, -0.8086757660))
|
|
125
|
+
_OKLAB_M1_INV = _mat_inv(_OKLAB_M1)
|
|
126
|
+
_OKLAB_M2_INV = _mat_inv(_OKLAB_M2)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _cbrt(x: float) -> float:
|
|
130
|
+
return math.copysign(abs(x) ** (1.0 / 3.0), x)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def linear_to_oklab(rgb):
|
|
134
|
+
"""Linear sRGB (extended) → Oklab ``(L, a, b)``; white (1, 1, 1) is L = 1."""
|
|
135
|
+
lms = _mat_mul_vec(_OKLAB_M1, rgb)
|
|
136
|
+
return _mat_mul_vec(_OKLAB_M2, tuple(_cbrt(c) for c in lms))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def oklab_to_linear(lab):
|
|
140
|
+
"""Oklab ``(L, a, b)`` → linear sRGB (extended)."""
|
|
141
|
+
lms = _mat_mul_vec(_OKLAB_M2_INV, lab)
|
|
142
|
+
return _mat_mul_vec(_OKLAB_M1_INV, tuple(c * c * c for c in lms))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def oklab_hue(rgb) -> float:
|
|
146
|
+
"""Oklab hue angle (radians) of a linear sRGB colour."""
|
|
147
|
+
_, a, b = linear_to_oklab(rgb)
|
|
148
|
+
return math.atan2(b, a)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def oklab_max_chroma(lightness: float, hue: float, peak: float = 1.0,
|
|
152
|
+
steps: int = 40) -> float:
|
|
153
|
+
"""The largest Oklab chroma at ``(lightness, hue)`` whose colour still
|
|
154
|
+
fits the DISPLAY-P3 box ``[0, peak]³`` in linear light — ``peak`` = the
|
|
155
|
+
brightest white the scale may reach, in multiples of the desktop's
|
|
156
|
+
white (1 = the SDR gamut). This is the gamut boundary an HDR colour
|
|
157
|
+
scale rides: it pinches to grey at black AND at the peak, so a ramp of
|
|
158
|
+
max-chroma colours whitens PROGRESSIVELY toward its top instead of
|
|
159
|
+
switching to white. Bisection: chroma grows monotonically out of the
|
|
160
|
+
box along a ray of fixed lightness and hue."""
|
|
161
|
+
if lightness <= 0.0 or lightness >= _cbrt(peak):
|
|
162
|
+
return 0.0
|
|
163
|
+
ca, sa = math.cos(hue), math.sin(hue)
|
|
164
|
+
|
|
165
|
+
def fits(chroma):
|
|
166
|
+
p3_rgb = linear_srgb_to_p3(oklab_to_linear((lightness, chroma * ca, chroma * sa)))
|
|
167
|
+
return all(-1e-6 <= c <= peak + 1e-6 for c in p3_rgb)
|
|
168
|
+
|
|
169
|
+
lo, hi = 0.0, 1.0 + _cbrt(peak)
|
|
170
|
+
if fits(hi):
|
|
171
|
+
return hi
|
|
172
|
+
for _ in range(steps):
|
|
173
|
+
mid = (lo + hi) * 0.5
|
|
174
|
+
if fits(mid):
|
|
175
|
+
lo = mid
|
|
176
|
+
else:
|
|
177
|
+
hi = mid
|
|
178
|
+
return lo
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Helpers for writing colours
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def white(scale: float):
|
|
187
|
+
"""A neutral ``scale`` × the SDR reference white, as an extended-sRGB
|
|
188
|
+
tuple: ``white(1)`` is sRGB white, ``white(4)`` is 1000 nits on a 250-nit
|
|
189
|
+
desktop. The helper for "how bright", independent of hue."""
|
|
190
|
+
e = linear_to_srgb(float(scale))
|
|
191
|
+
return (e, e, e)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def p3(r: float, g: float, b: float, scale: float = 1.0):
|
|
195
|
+
"""A Display-P3 colour (P3 primaries, sRGB curve — the CSS ``display-p3``
|
|
196
|
+
triple) as an extended-sRGB tuple. ``scale`` multiplies the linear light,
|
|
197
|
+
so ``p3(1, 0, 0, scale=16)`` is P3 red at 16 × reference white."""
|
|
198
|
+
lin = tuple(srgb_to_linear(c) * scale for c in (r, g, b))
|
|
199
|
+
return tuple(linear_to_srgb(c) for c in linear_p3_to_srgb(lin))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def scale_saturation(saturation: float, factor: float) -> float:
|
|
203
|
+
"""``saturation * factor`` for an EXTENDED-HSV saturation (colorsys on an
|
|
204
|
+
extended-sRGB tuple: s > 1 means a channel below 0 — outside the sRGB
|
|
205
|
+
gamut), capped at the colour's OWN gamut edge: 1.0 for a tint inside
|
|
206
|
+
sRGB, its own s for a wider one. A boost factor above 1 (the bg ramps'
|
|
207
|
+
1.1, the gutter's 1.05) must not push an sRGB primary out past P3 —
|
|
208
|
+
the packer clips per channel in linear P3, so an sRGB red tint and a
|
|
209
|
+
P3 red tint landed on the SAME P3-edge colour and P3 tints looked no
|
|
210
|
+
more saturated than sRGB ones (09-07). Before HDR imgui's u32 clamp
|
|
211
|
+
capped every boost at the sRGB edge; this keeps that look for SDR
|
|
212
|
+
tints and lets a wide tint keep exactly the chroma it was given."""
|
|
213
|
+
if saturation <= 0.0:
|
|
214
|
+
return 0.0
|
|
215
|
+
return min(max(1.0, saturation), saturation * factor)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def scale(color, k: float):
|
|
219
|
+
"""``color`` (an extended-sRGB tuple) with its linear light multiplied by
|
|
220
|
+
``k``; alpha, if present, is kept."""
|
|
221
|
+
rgb = tuple(linear_to_srgb(srgb_to_linear(c) * k) for c in color[:3])
|
|
222
|
+
return rgb + tuple(color[3:])
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# The u32 pipe
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
SDR_BIT = 0x80000000
|
|
230
|
+
ALPHA_MASK = 0x7F000000
|
|
231
|
+
RGB_MASK = 0x00FFFFFF
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _curve_params():
|
|
235
|
+
"""(range, octaves) of the HDR byte curve — Toggles.HDR, read live so a
|
|
236
|
+
toggle edit reaches the packer and the shaders (uniforms) together."""
|
|
237
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
238
|
+
return float(Toggles.HDR.vertex_range), float(Toggles.HDR.vertex_octaves)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _encode_hdr_byte(v: float, rng: float, octaves: float) -> int:
|
|
242
|
+
"""Linear P3 component → byte on the log curve: code 0 is exactly 0,
|
|
243
|
+
codes 1..255 span [range · 2^-octaves, range] evenly in log2."""
|
|
244
|
+
if v <= 0.0:
|
|
245
|
+
return 0
|
|
246
|
+
t = 1.0 + math.log2(v / rng) / octaves # 1.0 at the ceiling
|
|
247
|
+
code = int(1.0 + 254.0 * t + 0.5)
|
|
248
|
+
return 0 if code < 1 else (255 if code > 255 else code)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _decode_hdr_byte(code: int, rng: float, octaves: float) -> float:
|
|
252
|
+
if code <= 0:
|
|
253
|
+
return 0.0
|
|
254
|
+
return rng * 2.0 ** (octaves * ((code - 1) / 254.0 - 1.0))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# (r, g, b, alpha7) -> packed, HDR branch only. Cleared when the curve
|
|
258
|
+
# toggles change (clear_cache) - hotswap keeps the dict.
|
|
259
|
+
_HDR_PACK_CACHE = globals().get("_HDR_PACK_CACHE", {})
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def clear_cache() -> None:
|
|
263
|
+
"""Forget memoized HDR packs — call after editing Toggles.HDR.vertex_*."""
|
|
264
|
+
_HDR_PACK_CACHE.clear()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _is_sdr(r, g, b) -> bool:
|
|
268
|
+
return 0.0 <= r <= 1.0 and 0.0 <= g <= 1.0 and 0.0 <= b <= 1.0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def pack_color(r: float, g: float, b: float, a: float = 1.0) -> int:
|
|
272
|
+
"""Extended-sRGB components → imgui u32. The drop-in for
|
|
273
|
+
``imgui.get_color_u32_rgba``: identical bytes for a colour inside [0, 1]
|
|
274
|
+
(bar the 7-bit alpha), the HDR layout for anything outside it."""
|
|
275
|
+
if a != a:
|
|
276
|
+
a = 0.0
|
|
277
|
+
alpha7 = int(a * 127.0 + 0.5)
|
|
278
|
+
alpha7 = 0 if alpha7 < 0 else (127 if alpha7 > 127 else alpha7)
|
|
279
|
+
if _is_sdr(r, g, b):
|
|
280
|
+
return (SDR_BIT | (alpha7 << 24)
|
|
281
|
+
| (int(b * 255.0 + 0.5) << 16)
|
|
282
|
+
| (int(g * 255.0 + 0.5) << 8)
|
|
283
|
+
| int(r * 255.0 + 0.5))
|
|
284
|
+
# Tiny negative noise from colour math (a computed purple tint like
|
|
285
|
+
# (0, -0.005, -0.009)) is not a wide-gamut intent: snap it to SDR.
|
|
286
|
+
if r >= -0.02 and g >= -0.02 and b >= -0.02 and r <= 1.0 and g <= 1.0 and b <= 1.0:
|
|
287
|
+
return pack_color(max(0.0, r), max(0.0, g), max(0.0, b), a)
|
|
288
|
+
# The HDR path is ~25 us of matrix + log math. HDR colours are few
|
|
289
|
+
# distinct values but many samples, so memoize on the exact inputs.
|
|
290
|
+
key = (r, g, b, alpha7)
|
|
291
|
+
packed = _HDR_PACK_CACHE.get(key)
|
|
292
|
+
if packed is not None:
|
|
293
|
+
return packed
|
|
294
|
+
rng, octaves = _curve_params()
|
|
295
|
+
lin = linear_srgb_to_p3((srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)))
|
|
296
|
+
codes = [_encode_hdr_byte(min(rng, max(0.0, c)), rng, octaves) for c in lin]
|
|
297
|
+
packed = (alpha7 << 24) | (codes[2] << 16) | (codes[1] << 8) | codes[0]
|
|
298
|
+
if len(_HDR_PACK_CACHE) > 4096:
|
|
299
|
+
_HDR_PACK_CACHE.clear()
|
|
300
|
+
_HDR_PACK_CACHE[key] = packed
|
|
301
|
+
return packed
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def unpack_color(packed: int):
|
|
305
|
+
"""imgui u32 → extended-sRGB ``(r, g, b, a)``. Inverse of ``pack_color``
|
|
306
|
+
up to the byte quantization."""
|
|
307
|
+
packed &= 0xFFFFFFFF
|
|
308
|
+
a = ((packed >> 24) & 0x7F) / 127.0
|
|
309
|
+
rb, gb, bb = packed & 0xFF, (packed >> 8) & 0xFF, (packed >> 16) & 0xFF
|
|
310
|
+
if packed & SDR_BIT:
|
|
311
|
+
return (rb / 255.0, gb / 255.0, bb / 255.0, a)
|
|
312
|
+
rng, octaves = _curve_params()
|
|
313
|
+
lin_p3 = tuple(_decode_hdr_byte(c, rng, octaves) for c in (rb, gb, bb))
|
|
314
|
+
return tuple(linear_to_srgb(c) for c in linear_p3_to_srgb(lin_p3)) + (a,)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def is_sdr_packed(packed: int) -> bool:
|
|
318
|
+
return bool(packed & SDR_BIT)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def with_alpha(packed: int, a: float) -> int:
|
|
322
|
+
"""``packed`` with its alpha replaced — the flag and RGB bytes kept."""
|
|
323
|
+
alpha7 = int(a * 127.0 + 0.5)
|
|
324
|
+
alpha7 = 0 if alpha7 < 0 else (127 if alpha7 > 127 else alpha7)
|
|
325
|
+
return (packed & (SDR_BIT | RGB_MASK)) | (alpha7 << 24)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def packed_alpha(packed: int) -> float:
|
|
329
|
+
return ((packed >> 24) & 0x7F) / 127.0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def scale_alpha(packed: int, k: float) -> int:
|
|
333
|
+
"""``packed`` with its alpha multiplied by ``k`` (the fade helper)."""
|
|
334
|
+
return with_alpha(packed, packed_alpha(packed) * k)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def style_color(r: float, g: float, b: float, a: float = 1.0):
|
|
338
|
+
"""The float4 to store in an imgui STYLE entry for this colour: imgui
|
|
339
|
+
converts style floats to u32 with ``round(x * 255)`` per channel, so the
|
|
340
|
+
stored floats are the packed bytes / 255 and the conversion lands on
|
|
341
|
+
exactly the bytes ``pack_color`` would produce."""
|
|
342
|
+
packed = pack_color(r, g, b, a)
|
|
343
|
+
return ((packed & 0xFF) / 255.0, ((packed >> 8) & 0xFF) / 255.0,
|
|
344
|
+
((packed >> 16) & 0xFF) / 255.0, ((packed >> 24) & 0xFF) / 255.0)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
# GLSL
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
# Splice this into any vertex shader that reads imgui's `Color` attribute
|
|
352
|
+
# (normalized ubytes). `MeltyHdrRange` / `MeltyHdrOctaves` are uniforms so a
|
|
353
|
+
# Toggles.HDR edit reaches the shader live - set them with
|
|
354
|
+
# `set_decode_uniforms(program)` each draw.
|
|
355
|
+
GLSL_DECODE = """
|
|
356
|
+
uniform float MeltyHdrRange; // Toggles.HDR.vertex_range
|
|
357
|
+
uniform float MeltyHdrOctaves; // Toggles.HDR.vertex_octaves
|
|
358
|
+
const mat3 MELTY_P3_TO_SRGB = mat3(%s);
|
|
359
|
+
|
|
360
|
+
vec3 melty_srgb_to_linear(vec3 c) {
|
|
361
|
+
vec3 lo = c / 12.92;
|
|
362
|
+
vec3 hi = pow((c + 0.055) / 1.055, vec3(2.4));
|
|
363
|
+
return mix(lo, hi, step(0.04045, c));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// imgui vertex colour (normalized ubytes) -> linear scRGB + alpha.
|
|
367
|
+
vec4 melty_decode_color(vec4 c) {
|
|
368
|
+
float abyte = c.a * 255.0;
|
|
369
|
+
bool sdr = abyte >= 127.5;
|
|
370
|
+
float alpha = (sdr ? abyte - 128.0 : abyte) / 127.0;
|
|
371
|
+
vec3 rgb;
|
|
372
|
+
if (sdr) {
|
|
373
|
+
rgb = melty_srgb_to_linear(c.rgb);
|
|
374
|
+
} else {
|
|
375
|
+
vec3 code = c.rgb * 255.0;
|
|
376
|
+
vec3 v = MeltyHdrRange * exp2(MeltyHdrOctaves * ((code - 1.0) / 254.0 - 1.0));
|
|
377
|
+
v *= step(0.5, code); // code 0 is exactly 0
|
|
378
|
+
rgb = MELTY_P3_TO_SRGB * v;
|
|
379
|
+
}
|
|
380
|
+
return vec4(rgb, alpha);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// The VARYING form: rgb PREMULTIPLIED by alpha. imgui's anti-aliased fills
|
|
384
|
+
// and strokes give the feather's outer vertices the same RGB bytes with an
|
|
385
|
+
// alpha BYTE of 0 — which clears the SDR bit, so decoded per vertex those
|
|
386
|
+
// bytes read as an HDR log code (a 61/255 grey becomes linear 0.11, four
|
|
387
|
+
// times brighter) and the rasterizer interpolates that brightness across
|
|
388
|
+
// the feather: a light 1-px fringe around every rounded rect (09-07). A
|
|
389
|
+
// zero-alpha vertex must contribute NOTHING to the colour, which is what
|
|
390
|
+
// premultiplied interpolation does; the fragment stage divides the alpha
|
|
391
|
+
// back out (melty_unpremultiply) so the blend stays straight-alpha.
|
|
392
|
+
vec4 melty_decode_premultiplied(vec4 c) {
|
|
393
|
+
vec4 d = melty_decode_color(c);
|
|
394
|
+
return vec4(d.rgb * d.a, d.a);
|
|
395
|
+
}
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
# Fragment-side inverse of melty_decode_premultiplied (the fragment shaders
|
|
399
|
+
# don't include GLSL_DECODE, so this is spliced on its own).
|
|
400
|
+
GLSL_UNPREMULTIPLY = """
|
|
401
|
+
vec4 melty_unpremultiply(vec4 f) {
|
|
402
|
+
return vec4(f.rgb / max(f.a, 1e-6), f.a);
|
|
403
|
+
}
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _glsl_mat3(m) -> str:
|
|
408
|
+
# GLSL mat3 is column-major: list column by column.
|
|
409
|
+
cols = [[m[r][c] for r in range(3)] for c in range(3)]
|
|
410
|
+
return ", ".join(f"{v:.7f}" for col in cols for v in col)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
GLSL_DECODE = GLSL_DECODE % _glsl_mat3(P3_TO_SRGB)
|
|
414
|
+
|
|
415
|
+
# Text brightness ceiling (Toggles.HDR.text_max_stops): scales the glyph's
|
|
416
|
+
# colour down uniformly - hue and saturation kept, P3 negatives ride along -
|
|
417
|
+
# until its brightest channel sits at MeltyTextMax (multiples of reference
|
|
418
|
+
# white). Spliced into the fragment stage of the draw-list renderer (glyph
|
|
419
|
+
# pixels only) and the text-texture bake; the uniform rides with
|
|
420
|
+
# set_decode_uniforms so both stay live with the toggle.
|
|
421
|
+
GLSL_TEXT_CLAMP = """
|
|
422
|
+
uniform float MeltyTextMax; // 2^Toggles.HDR.text_max_stops
|
|
423
|
+
|
|
424
|
+
vec3 melty_clamp_text(vec3 c) {
|
|
425
|
+
float peak = max(max(c.r, c.g), c.b);
|
|
426
|
+
return peak > MeltyTextMax ? c * (MeltyTextMax / peak) : c;
|
|
427
|
+
}
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def set_decode_uniforms(program: int) -> None:
|
|
432
|
+
"""Set the curve uniforms `GLSL_DECODE` declares on a bound program."""
|
|
433
|
+
import OpenGL.GL as gl
|
|
434
|
+
rng, octaves = _curve_params()
|
|
435
|
+
loc = gl.glGetUniformLocation(program, "MeltyHdrRange")
|
|
436
|
+
if loc >= 0:
|
|
437
|
+
gl.glUniform1f(loc, rng)
|
|
438
|
+
loc = gl.glGetUniformLocation(program, "MeltyHdrOctaves")
|
|
439
|
+
if loc >= 0:
|
|
440
|
+
gl.glUniform1f(loc, octaves)
|
|
441
|
+
loc = gl.glGetUniformLocation(program, "MeltyTextMax") # GLSL_TEXT_CLAMP
|
|
442
|
+
if loc >= 0:
|
|
443
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
444
|
+
gl.glUniform1f(loc, float(2.0 ** float(Toggles.HDR.text_max_stops)))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Linear scRGB -> display encodings, for the presentation pass.
|
|
448
|
+
GLSL_ENCODE = """
|
|
449
|
+
vec3 melty_linear_to_srgb(vec3 v) {
|
|
450
|
+
v = clamp(v, 0.0, 1.0);
|
|
451
|
+
vec3 lo = v * 12.92;
|
|
452
|
+
vec3 hi = 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055;
|
|
453
|
+
return mix(lo, hi, step(0.0031308, v));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// BT.709/sRGB primaries -> BT.2020 primaries (linear, D65).
|
|
457
|
+
const mat3 MELTY_SRGB_TO_BT2020 = mat3(
|
|
458
|
+
0.6274040, 0.0690970, 0.0163916,
|
|
459
|
+
0.3292820, 0.9195400, 0.0880132,
|
|
460
|
+
0.0433136, 0.0113612, 0.8955950);
|
|
461
|
+
|
|
462
|
+
// SMPTE ST 2084 (PQ) encode of linear light in NITS.
|
|
463
|
+
vec3 melty_pq_encode(vec3 nits) {
|
|
464
|
+
const float m1 = 0.1593017578125, m2 = 78.84375;
|
|
465
|
+
const float c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;
|
|
466
|
+
vec3 y = clamp(nits / 10000.0, 0.0, 1.0);
|
|
467
|
+
vec3 yp = pow(y, vec3(m1));
|
|
468
|
+
return pow((c1 + c2 * yp) / (1.0 + c3 * yp), vec3(m2));
|
|
469
|
+
}
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# ---------------------------------------------------------------------------
|
|
474
|
+
# The wide-gamut picker's colour model (draw_color_picker's "Wide" tab)
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
#
|
|
477
|
+
# The picker works in DISPLAY P3 HSV plus an EXPOSURE: (h, s, v) is the
|
|
478
|
+
# classic square on P3 primaries (P3 saturates further than sRGB), and
|
|
479
|
+
# exposure is a linear multiplier above white - v = 1 with exposure 4 is
|
|
480
|
+
# white(4). Every displayable colour has one such tuple: the P3 gamut
|
|
481
|
+
# contains sRGB, so anything the display can show packs non-negative in P3.
|
|
482
|
+
|
|
483
|
+
import colorsys as _colorsys
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def p3_hsv_from_extended(r: float, g: float, b: float):
|
|
487
|
+
"""Extended-sRGB → (h, s, v, exposure) in P3 HSV. A component outside P3
|
|
488
|
+
(negative after the primaries conversion) clips to the P3 edge."""
|
|
489
|
+
lin = linear_srgb_to_p3((srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)))
|
|
490
|
+
lin = tuple(max(0.0, c) for c in lin)
|
|
491
|
+
peak = max(lin)
|
|
492
|
+
exposure = peak if peak > 1.0 else 1.0
|
|
493
|
+
enc = tuple(linear_to_srgb(c / exposure) for c in lin) # Display P3 uses the sRGB curve
|
|
494
|
+
h, s, v = _colorsys.rgb_to_hsv(*enc)
|
|
495
|
+
return h, s, v, exposure
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def extended_from_p3_hsv(h: float, s: float, v: float, exposure: float = 1.0):
|
|
499
|
+
"""(h, s, v, exposure) in P3 HSV → extended-sRGB (r, g, b)."""
|
|
500
|
+
enc = _colorsys.hsv_to_rgb(h, s, v)
|
|
501
|
+
lin = tuple(srgb_to_linear(c) * exposure for c in enc)
|
|
502
|
+
return tuple(linear_to_srgb(c) for c in linear_p3_to_srgb(lin))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def wide_square_linear(hue: float, size: int, top_fraction: float, max_stops: float):
|
|
506
|
+
"""The picker square for one hue as a (size, size, 4) float32 array of
|
|
507
|
+
LINEAR scRGB (row 0 = top). X is P3 saturation 0→1. The top
|
|
508
|
+
`top_fraction` of the rows is exposure: 2^max_stops at the top down to
|
|
509
|
+
1.0 (white) at the seam; the rest is the classic value axis 1→0."""
|
|
510
|
+
import numpy as np
|
|
511
|
+
n = int(size)
|
|
512
|
+
top = max(1, int(round(n * top_fraction)))
|
|
513
|
+
s = np.linspace(0.0, 1.0, n, dtype=np.float32)[None, :, None] # columns
|
|
514
|
+
rows = np.arange(n, dtype=np.float32)
|
|
515
|
+
v = np.ones(n, dtype=np.float32)
|
|
516
|
+
exposure = np.ones(n, dtype=np.float32)
|
|
517
|
+
exposure[:top] = 2.0 ** (max_stops * (1.0 - rows[:top] / top))
|
|
518
|
+
v[top:] = 1.0 - (rows[top:] - top) / max(1, n - top - 1)
|
|
519
|
+
v = np.clip(v, 0.0, 1.0)[:, None, None]
|
|
520
|
+
exposure = exposure[:, None, None]
|
|
521
|
+
hue_rgb = np.asarray(_colorsys.hsv_to_rgb(hue, 1.0, 1.0), dtype=np.float32)[None, None, :]
|
|
522
|
+
enc = v * (1.0 - s * (1.0 - hue_rgb)) # HSV at fixed hue, P3-encoded
|
|
523
|
+
lin = np.where(enc <= 0.04045, enc / 12.92, ((enc + 0.055) / 1.055) ** 2.4) * exposure
|
|
524
|
+
m = np.asarray(P3_TO_SRGB, dtype=np.float32)
|
|
525
|
+
out = np.empty((n, n, 4), dtype=np.float32)
|
|
526
|
+
out[..., :3] = lin @ m.T
|
|
527
|
+
out[..., 3] = 1.0
|
|
528
|
+
return out
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ---------------------------------------------------------------------------
|
|
532
|
+
# The "sRGB+" tab: the classic sRGB square, extended past its right edge
|
|
533
|
+
# ---------------------------------------------------------------------------
|
|
534
|
+
#
|
|
535
|
+
# The square is the classic sRGB HSV square, untouched. Each of its rows
|
|
536
|
+
# (an sRGB value v) ends at the sRGB gamut edge (hue, 1, v); the extension
|
|
537
|
+
# to its right carries that edge carried on in DISPLAY P3: same P3 hue,
|
|
538
|
+
# saturation from the edge's own P3 saturation up to 1 and value from the
|
|
539
|
+
# edge's P3 value up to the row's v - so the strip is colour-continuous
|
|
540
|
+
# at the seam, the top row's right edge is a pure P3 primary, and the
|
|
541
|
+
# extension shows all the colours sRGB cannot reach. Exposure
|
|
542
|
+
# (brightness above white) is not on the square: it rides through unchanged.
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def srgb_extension_edge(hue: float, v: float):
|
|
546
|
+
"""The sRGB gamut edge colour (hue, s=1, v) in P3 HSV: (hp, sp, vp)."""
|
|
547
|
+
r, g, b = _colorsys.hsv_to_rgb(hue, 1.0, v)
|
|
548
|
+
hp, sp, vp, _exposure = p3_hsv_from_extended(r, g, b)
|
|
549
|
+
return hp, sp, vp
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def extended_from_srgb_extension(hue: float, s: float, v: float, x: float, exposure: float = 1.0):
|
|
553
|
+
"""sRGB+ coordinates → extended-sRGB (r, g, b). (hue, s, v) is the
|
|
554
|
+
classic sRGB HSV square; x in [0, 1] is how far into the extension the
|
|
555
|
+
marker sits (0 = the square's right edge, only meaningful at s = 1)."""
|
|
556
|
+
if x <= 0.0:
|
|
557
|
+
rgb = _colorsys.hsv_to_rgb(hue, s, v)
|
|
558
|
+
return tuple(linear_to_srgb(srgb_to_linear(c) * exposure) for c in rgb)
|
|
559
|
+
hp, sp, vp = srgb_extension_edge(hue, v)
|
|
560
|
+
x = min(x, 1.0)
|
|
561
|
+
return extended_from_p3_hsv(hp, sp + (1.0 - sp) * x, vp + (v - vp) * x, exposure)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def srgb_extension_coords(r: float, g: float, b: float, hue_hint: float = None, steps: int = 6):
|
|
565
|
+
"""Extended-sRGB → (hue, s, v, x, exposure) of the sRGB+ strip, the
|
|
566
|
+
inverse of extended_from_srgb_extension. Inside sRGB it is the classic
|
|
567
|
+
HSV with x = 0. Outside, s = 1: for a hue, the row v is the bisection
|
|
568
|
+
root of the extension's P3 value (with x fixed by the P3 saturation)
|
|
569
|
+
against the target's, and the hue is corrected by a few secant steps
|
|
570
|
+
toward the target's P3 hue. hue_hint seeds that search (the cached hue keeps the marker
|
|
571
|
+
steady when the colour is gray)."""
|
|
572
|
+
lin = (srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b))
|
|
573
|
+
peak = max(lin)
|
|
574
|
+
srgb_exposure = peak if peak > 1.0 else 1.0
|
|
575
|
+
norm = tuple(c / srgb_exposure for c in lin)
|
|
576
|
+
if all(-1e-6 <= c <= 1.0 + 1e-6 for c in norm):
|
|
577
|
+
enc = tuple(linear_to_srgb(min(1.0, max(0.0, c))) for c in norm)
|
|
578
|
+
h, s, v = _colorsys.rgb_to_hsv(*enc)
|
|
579
|
+
return h, s, v, 0.0, srgb_exposure
|
|
580
|
+
hp, sp, vp, exposure = p3_hsv_from_extended(r, g, b)
|
|
581
|
+
vp_abs_lin = srgb_to_linear(vp) * exposure # the P3 peak in absolute linear light
|
|
582
|
+
vp_abs = linear_to_srgb(vp_abs_lin) # ... and sRGB-encoded (the HSV value scale)
|
|
583
|
+
if hue_hint is None:
|
|
584
|
+
enc = tuple(linear_to_srgb(min(1.0, max(0.0, c / exposure))) for c in lin)
|
|
585
|
+
hue_hint = _colorsys.rgb_to_hsv(*enc)[0]
|
|
586
|
+
|
|
587
|
+
def solve_row(h):
|
|
588
|
+
"""(v, x, exposure) for hue h: x is fixed per row by the P3
|
|
589
|
+
saturation; the row is the bisection root of the extension's P3
|
|
590
|
+
value against the target's ABSOLUTE peak — an HDR colour takes the
|
|
591
|
+
SDR row that reaches it when one does, else the top row with the
|
|
592
|
+
rest as exposure (exposure is a free split of the same colour)."""
|
|
593
|
+
def at(v):
|
|
594
|
+
_hp, sp_e, vp_e = srgb_extension_edge(h, v)
|
|
595
|
+
x = min(max((sp - sp_e) / max(1e-6, 1.0 - sp_e), 0.0), 1.0)
|
|
596
|
+
return vp_e + (v - vp_e) * x, x
|
|
597
|
+
top_value, top_x = at(1.0)
|
|
598
|
+
if top_value <= vp_abs:
|
|
599
|
+
# Top row + exposure. HSV saturation is not invariant under a
|
|
600
|
+
# linear scale (the sRGB curve's offset), and the target's sp
|
|
601
|
+
# was computed from the colour normalized to its peak - so find the
|
|
602
|
+
# x whose top-row colour, normalized the same way, has that
|
|
603
|
+
# saturation (monotone in x), and take the exposure from the
|
|
604
|
+
# ratio of their peaks.
|
|
605
|
+
hp_e, sp_e, vp_e = srgb_extension_edge(h, 1.0)
|
|
606
|
+
|
|
607
|
+
def normalized(x):
|
|
608
|
+
rgb = _colorsys.hsv_to_rgb(hp_e, sp_e + (1.0 - sp_e) * x, vp_e + (1.0 - vp_e) * x)
|
|
609
|
+
lin = tuple(srgb_to_linear(c) for c in rgb)
|
|
610
|
+
peak = max(lin)
|
|
611
|
+
enc = tuple(linear_to_srgb(c / max(1e-9, peak)) for c in lin)
|
|
612
|
+
return _colorsys.rgb_to_hsv(*enc)[1], peak
|
|
613
|
+
lo, hi = 0.0, 1.0
|
|
614
|
+
for _ in range(20):
|
|
615
|
+
mid = 0.5 * (lo + hi)
|
|
616
|
+
if normalized(mid)[0] < sp:
|
|
617
|
+
lo = mid
|
|
618
|
+
else:
|
|
619
|
+
hi = mid
|
|
620
|
+
x = 0.5 * (lo + hi)
|
|
621
|
+
return 1.0, x, vp_abs_lin / max(1e-9, normalized(x)[1])
|
|
622
|
+
lo, hi = 0.0, 1.0
|
|
623
|
+
for _ in range(18):
|
|
624
|
+
mid = 0.5 * (lo + hi)
|
|
625
|
+
if at(mid)[0] < vp_abs:
|
|
626
|
+
lo = mid
|
|
627
|
+
else:
|
|
628
|
+
hi = mid
|
|
629
|
+
v = 0.5 * (lo + hi)
|
|
630
|
+
return v, at(v)[1], 1.0
|
|
631
|
+
|
|
632
|
+
def wrap(d):
|
|
633
|
+
return (d + 0.5) % 1.0 - 0.5
|
|
634
|
+
|
|
635
|
+
def residual(h):
|
|
636
|
+
v, x, e = solve_row(h)
|
|
637
|
+
return wrap(hp - srgb_extension_edge(h, v)[0]), v, x, e
|
|
638
|
+
|
|
639
|
+
h = hue_hint
|
|
640
|
+
delta, v, x, e = residual(h)
|
|
641
|
+
for _ in range(steps):
|
|
642
|
+
if abs(delta) < 1e-7:
|
|
643
|
+
break
|
|
644
|
+
# secant step: the sRGB→P3 hue slope is ~0.5 around green, ~1 elsewhere
|
|
645
|
+
probe = 1e-3
|
|
646
|
+
slope = wrap(srgb_extension_edge(h + probe, v)[0] - srgb_extension_edge(h, v)[0]) / probe
|
|
647
|
+
h = (h + delta / min(max(slope, 0.25), 4.0)) % 1.0
|
|
648
|
+
delta, v, x, e = residual(h)
|
|
649
|
+
if abs(delta) > 1e-4:
|
|
650
|
+
# The secant wandered (a poor hint across a hue sector): bracket the
|
|
651
|
+
# sign change of the residual over the hue circle and bisect it -
|
|
652
|
+
# the sRGB→P3 hue map is monotone, so exactly one bracket holds it.
|
|
653
|
+
n = 24
|
|
654
|
+
samples = [(i / n, residual(i / n)[0]) for i in range(n)]
|
|
655
|
+
lo, hi = None, None
|
|
656
|
+
for i in range(n):
|
|
657
|
+
h0, d0 = samples[i]
|
|
658
|
+
h1, d1 = samples[(i + 1) % n]
|
|
659
|
+
if d0 >= 0.0 > d1 or (d0 >= 0.0 and d1 >= 0.0 and d1 < d0 - 0.5):
|
|
660
|
+
lo, hi = h0, h0 + 1.0 / n
|
|
661
|
+
break
|
|
662
|
+
if lo is None:
|
|
663
|
+
lo, hi = 0.0, 1.0
|
|
664
|
+
for _ in range(22):
|
|
665
|
+
mid = 0.5 * (lo + hi)
|
|
666
|
+
if residual(mid % 1.0)[0] >= 0.0:
|
|
667
|
+
lo = mid
|
|
668
|
+
else:
|
|
669
|
+
hi = mid
|
|
670
|
+
h = (0.5 * (lo + hi)) % 1.0
|
|
671
|
+
_d, v, x, e = residual(h)
|
|
672
|
+
return h, 1.0, v, x, e
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def srgb_extension_linear(hue: float, rows: int, cols: int):
|
|
676
|
+
"""The extension strip for one sRGB hue as a (rows, cols, 4) float32
|
|
677
|
+
array of LINEAR scRGB (row 0 = top = v 1, bottom = black; column 0 =
|
|
678
|
+
the sRGB edge, last column = P3 saturation 1 at the row's value)."""
|
|
679
|
+
import numpy as np
|
|
680
|
+
n_rows, n_cols = int(rows), int(cols)
|
|
681
|
+
x = np.linspace(0.0, 1.0, n_cols, dtype=np.float32)[None, :, None]
|
|
682
|
+
v_row = np.empty(n_rows, dtype=np.float32)
|
|
683
|
+
hp = np.empty(n_rows, dtype=np.float32)
|
|
684
|
+
sp = np.empty(n_rows, dtype=np.float32)
|
|
685
|
+
vp = np.empty(n_rows, dtype=np.float32)
|
|
686
|
+
for i in range(n_rows):
|
|
687
|
+
v_row[i] = 1.0 - i / max(1, n_rows - 1)
|
|
688
|
+
hp[i], sp[i], vp[i] = srgb_extension_edge(hue, float(v_row[i]))
|
|
689
|
+
hue_rgb = np.asarray([_colorsys.hsv_to_rgb(float(h), 1.0, 1.0) for h in hp],
|
|
690
|
+
dtype=np.float32)[:, None, :] # per-row P3 hue
|
|
691
|
+
s = sp[:, None, None] + (1.0 - sp[:, None, None]) * x # P3 saturation past the edge
|
|
692
|
+
vv = vp[:, None, None] + (v_row[:, None, None] - vp[:, None, None]) * x # P3 value up to the row's v
|
|
693
|
+
enc = vv * (1.0 - s * (1.0 - hue_rgb)) # HSV at that position, P3-encoded
|
|
694
|
+
lin = np.where(enc <= 0.04045, enc / 12.92, ((enc + 0.055) / 1.055) ** 2.4)
|
|
695
|
+
m = np.asarray(P3_TO_SRGB, dtype=np.float32)
|
|
696
|
+
out = np.empty((n_rows, n_cols, 4), dtype=np.float32)
|
|
697
|
+
out[..., :3] = lin @ m.T
|
|
698
|
+
out[..., 3] = 1.0
|
|
699
|
+
return out
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def srgb_plus_linear(hue: float, square: int, ext: int, band: int, max_stops: float):
|
|
703
|
+
"""The sRGB+ tab's whole picking area for one hue as a
|
|
704
|
+
(band + square, square + ext, 4) float32 array of LINEAR scRGB: the
|
|
705
|
+
classic sRGB square (rows band.., cols ..square), the P3 extension to
|
|
706
|
+
its right (srgb_extension_linear), and above both an exposure band —
|
|
707
|
+
the top row (v = 1) of each lifted by 2^max_stops at the top down to
|
|
708
|
+
1.0 at the seam, so the band's bottom row IS the square's top row."""
|
|
709
|
+
import numpy as np
|
|
710
|
+
n_sq, n_ext, n_band = int(square), int(ext), int(band)
|
|
711
|
+
out = np.empty((n_band + n_sq, n_sq + n_ext, 4), dtype=np.float32)
|
|
712
|
+
out[..., 3] = 1.0
|
|
713
|
+
# the classic square, sRGB-encoded (row 0 = v 1, col 0 = s 0)
|
|
714
|
+
s_axis = np.linspace(0.0, 1.0, n_sq, dtype=np.float32)[None, :, None]
|
|
715
|
+
v_axis = np.linspace(1.0, 0.0, n_sq, dtype=np.float32)[:, None, None]
|
|
716
|
+
hue_rgb = np.asarray(_colorsys.hsv_to_rgb(hue, 1.0, 1.0), dtype=np.float32)[None, None, :]
|
|
717
|
+
enc = v_axis * (1.0 - s_axis * (1.0 - hue_rgb))
|
|
718
|
+
square_lin = np.where(enc <= 0.04045, enc / 12.92, ((enc + 0.055) / 1.055) ** 2.4)
|
|
719
|
+
out[n_band:, :n_sq, :3] = square_lin
|
|
720
|
+
if n_ext:
|
|
721
|
+
out[n_band:, n_sq:, :] = srgb_extension_linear(hue, n_sq, n_ext)
|
|
722
|
+
if n_band:
|
|
723
|
+
rows = np.arange(n_band, dtype=np.float32)
|
|
724
|
+
exposure = (2.0 ** (max_stops * (1.0 - rows / n_band)))[:, None, None]
|
|
725
|
+
out[:n_band, :, :3] = out[n_band, :, :3][None, :, :] * exposure
|
|
726
|
+
return out
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def srgb_saturation_limit(hue: float, v: float, steps: int = 14) -> float:
|
|
730
|
+
"""Largest P3 saturation at (hue, v) whose colour still sits inside the
|
|
731
|
+
sRGB gamut (bisection; 1.0 when every saturation fits, e.g. at black)."""
|
|
732
|
+
def inside(s):
|
|
733
|
+
lin = linear_p3_to_srgb(tuple(srgb_to_linear(c) for c in _colorsys.hsv_to_rgb(hue, s, v)))
|
|
734
|
+
return all(-1e-4 <= c <= 1.0 + 1e-4 for c in lin)
|
|
735
|
+
if inside(1.0):
|
|
736
|
+
return 1.0
|
|
737
|
+
lo, hi = 0.0, 1.0
|
|
738
|
+
for _ in range(steps):
|
|
739
|
+
mid = 0.5 * (lo + hi)
|
|
740
|
+
if inside(mid):
|
|
741
|
+
lo = mid
|
|
742
|
+
else:
|
|
743
|
+
hi = mid
|
|
744
|
+
return lo
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def srgb_region_outline(hue: float, top_fraction: float, samples: int = 32):
|
|
748
|
+
"""The sRGB-reachable region of the wide square for one hue, as a list
|
|
749
|
+
of (x, y) fractions of the square: along the white seam from the left
|
|
750
|
+
edge to the gamut edge, then down the gamut edge to black."""
|
|
751
|
+
points = [(0.0, top_fraction)]
|
|
752
|
+
for i in range(samples + 1):
|
|
753
|
+
v = 1.0 - i / samples
|
|
754
|
+
x = srgb_saturation_limit(hue, v)
|
|
755
|
+
y = top_fraction + (1.0 - v) * (1.0 - top_fraction)
|
|
756
|
+
points.append((x, y))
|
|
757
|
+
return points
|