From 4dd9493135978ce638cb70e6ff22556758bf2ae8 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 8 May 2023 21:16:26 +0200 Subject: [PATCH] Use Oklab for text and cursor contrast adjustments (#15283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oklab by Björn Ottosson is a color space that has less irregularities than the CIELAB color space used for ΔE2000. The distance metric for Oklab (ΔEOK) is defined by CSS4 as the simple euclidian distance. This allows us to drastically simplify the code needed to determine a color that has enough contrast. The new implementation still lacks proper gamut mapping, but that's another and less important issue. I also made it so that text with the dim attribute gets adjusted just like regular text, since this is an accessibility feature after all. The new code is so much faster than the old code (12-125x) that I dropped any caching code we had. While this increases the CPU overhead when printing lots of indexed colors, the code is way less complex now. "Increases" in this case however means something in the order of 15-60ns per color change (as measured on my CPU). It's possible to further improve the performance using explicit SIMD instructions, but I've left that as a future improvement, since that will make the code quite a bit more verbose and I didn't want to hinder the initial review. Finally, these new routines are also used for ensuring that the AtlasEngine cursors remains visible at all times. Closes #9610 ## Validation Steps Performed * When `adjustIndistinguishableColors` is enabled colors are distinguishable ✅ * An inverted cursor on top of a `#7f7f7f` foreground & background is still visible ✅ * A colored cursor on top of a background with identical color is still visible ✅ * Cursors on a transparent background are visible ✅ --- .github/actions/spelling/allow/names.txt | 7 +- .github/actions/spelling/excludes.txt | 1 + .github/actions/spelling/expect/expect.txt | 8 + NOTICE.md | 49 +-- doc/color_nudging.html | 235 ++++++++++++ src/buffer/out/TextColor.cpp | 7 +- src/buffer/out/TextColor.h | 11 +- src/cascadia/TerminalCore/Terminal.cpp | 3 - src/renderer/atlas/BackendD3D.cpp | 31 +- src/renderer/atlas/BackendD3D.h | 4 +- src/renderer/base/RenderSettings.cpp | 123 ++----- src/renderer/inc/RenderSettings.hpp | 2 - src/terminal/adapter/adaptDispatch.cpp | 10 - src/types/ColorFix.cpp | 401 ++++++++++----------- src/types/inc/ColorFix.hpp | 60 +-- 15 files changed, 527 insertions(+), 425 deletions(-) create mode 100644 doc/color_nudging.html diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index a157774a53..e58e630b21 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -1,6 +1,6 @@ Anup -austdi arkthur +austdi Ballmer bhoj Bhojwani @@ -31,8 +31,8 @@ jerrysh Kaiyu kimwalisch KMehrain -KODELIFE Kodelife +KODELIFE Kourosh kowalczyk leonardder @@ -61,6 +61,7 @@ oising oldnewthing opengl osgwiki +Ottosson pabhojwa panos paulcam @@ -88,8 +89,8 @@ Wirt Wojciech zadjii Zamor -Zamora zamora +Zamora zljubisic Zoey zorio diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 3f6e31b107..3f3c4a691d 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -112,6 +112,7 @@ ^src/tools/RenderingTests/main.cpp$ ^src/tools/texttests/fira\.txt$ ^src/tools/U8U16Test/(?:fr|ru|zh)\.txt$ +^src/types/ColorFix.cpp ^src/types/ut_types/UtilsTests.cpp$ ^tools/ReleaseEngineering/ServicingPipeline.ps1$ ignore$ diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 8c4bfc3629..f471a83125 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -43,6 +43,7 @@ antialiasing ANull anycpu APARTMENTTHREADED +APCA APCs APIENTRY apiset @@ -194,6 +195,8 @@ chh chk CHT Cic +cielab +Cielab Clcompile CLE cleartype @@ -584,6 +587,7 @@ ENU ENUMLOGFONT ENUMLOGFONTEX enumranges +EOK eplace EPres EQU @@ -1290,6 +1294,8 @@ OEMFONT OEMFORMAT OEMs offboarded +oklab +Oklab OLEAUT OLECHAR onecore @@ -1770,6 +1776,8 @@ srcsrv SRCSRVTRG srctool srect +srgb +Srgb srv srvinit srvpipe diff --git a/NOTICE.md b/NOTICE.md index e504660457..fb48315d25 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -301,41 +301,28 @@ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## ConEmu -**Source**: [https://github.com/Maximus5/ConEmu](https://github.com/Maximus5/ConEmu) +## Oklab +**Source**: [https://bottosson.github.io/posts/oklab/](https://bottosson.github.io/posts/oklab/) ### License ``` -BSD 3-Clause License - -Copyright (c) 2009-2017, Maximus5 -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) 2020 Björn Ottosson +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` # Microsoft Open Source diff --git a/doc/color_nudging.html b/doc/color_nudging.html new file mode 100644 index 0000000000..cfbbc4bd11 --- /dev/null +++ b/doc/color_nudging.html @@ -0,0 +1,235 @@ + + + + + Perceptual Color Nudging + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + +
InputWCAG21:
APCA:
ΔE2000
(ConEmu)
WCAG21:
APCA:
ΔEOKWCAG21:
APCA:
+
+
+

Input

+
+
+
+
+
+
+
+
+
+

ΔE2000 (ConEmu)

+
+
+
+
+
+
+
+
+
+

ΔEOK

+
+
+
+
+
+
+
+
+ + + + diff --git a/src/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp index 7440cc03d3..2e2e6f9fa0 100644 --- a/src/buffer/out/TextColor.cpp +++ b/src/buffer/out/TextColor.cpp @@ -64,7 +64,7 @@ bool TextColor::CanBeBrightened() const noexcept bool TextColor::IsLegacy() const noexcept { - return IsIndex16() || (IsIndex256() && _index < 16); + return (IsIndex16() || IsIndex256()) && _index < 16; } bool TextColor::IsIndex16() const noexcept @@ -82,6 +82,11 @@ bool TextColor::IsDefault() const noexcept return _meta == ColorType::IsDefault; } +bool TextColor::IsDefaultOrLegacy() const noexcept +{ + return _meta != ColorType::IsRgb && _index < 16; +} + bool TextColor::IsRgb() const noexcept { return _meta == ColorType::IsRgb; diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h index b975028760..0b71078fbf 100644 --- a/src/buffer/out/TextColor.h +++ b/src/buffer/out/TextColor.h @@ -37,12 +37,14 @@ Revision History: #include "WexTestClass.h" #endif +// The enum values being in this particular order allows the compiler to do some useful optimizations, +// like simplifying `IsIndex16() || IsIndex256()` into a simple range check without branching. enum class ColorType : BYTE { - IsIndex256 = 0x0, - IsIndex16 = 0x1, - IsDefault = 0x2, - IsRgb = 0x3 + IsDefault, + IsIndex16, + IsIndex256, + IsRgb }; enum class ColorAlias : size_t @@ -121,6 +123,7 @@ public: bool IsIndex16() const noexcept; bool IsIndex256() const noexcept; bool IsDefault() const noexcept; + bool IsDefaultOrLegacy() const noexcept; bool IsRgb() const noexcept; void SetColor(const COLORREF rgbColor) noexcept; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 99fa845347..c6fa30d002 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -180,7 +180,6 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance) { _renderSettings.SetColorTableEntry(i, til::color{ appearance.GetColorTableEntry(i) }); } - _renderSettings.MakeAdjustedColorArray(); auto cursorShape = CursorType::VerticalBar; switch (appearance.CursorShape()) @@ -1325,8 +1324,6 @@ void Terminal::ApplyScheme(const Scheme& colorScheme) _renderSettings.SetColorTableEntry(TextColor::CURSOR_COLOR, til::color{ colorScheme.CursorColor }); - _renderSettings.MakeAdjustedColorArray(); - // Tell the control that the scrollbar has somehow changed. Used as a // workaround to force the control to redraw any scrollbar marks whose color // may have changed. diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 9ecf9beac7..c4959521fa 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -10,6 +10,7 @@ #include #include "dwrite.h" +#include "../../types/inc/ColorFix.hpp" #if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS #include "colorbrewer.h" @@ -826,7 +827,7 @@ void BackendD3D::_flushQuads(const RenderingPayload& p) if (!_cursorRects.empty()) { - _drawCursorForeground(p); + _drawCursorForeground(); } // TODO: Shrink instances buffer @@ -1627,9 +1628,19 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p) static_cast(p.s->font->cellSize.x * (x1 - x0)), p.s->font->cellSize.y, }; - const auto isInverted = cursorColor == 0xffffffff; - const auto background = isInverted ? bg ^ 0xc0c0c0 : cursorColor; - const auto foreground = isInverted ? 0 : bg; + auto background = cursorColor; + auto foreground = bg; + + if (cursorColor == 0xffffffff) + { + background = bg ^ 0xffffff; + foreground = 0xffffffff; + } + + // The legacy console used to invert colors by just doing `bg ^ 0xc0c0c0`. This resulted + // in a minimum squared distance of just 0.029195 across all possible color combinations. + background = ColorFix::GetPerceivableColor(background, bg, 0.25f * 0.25f); + auto& c0 = _cursorRects.emplace_back(position, size, background, foreground); switch (static_cast(p.s->cursor->cursorType)) @@ -1702,7 +1713,7 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p) } } -void BackendD3D::_drawCursorForeground(const RenderingPayload& p) +void BackendD3D::_drawCursorForeground() { // NOTE: _appendQuad() may reallocate the _instances vector. It's important to iterate // by index, because pointers (or iterators) would get invalidated. It's also important @@ -1779,7 +1790,7 @@ void BackendD3D::_drawCursorForeground(const RenderingPayload& p) { // The _instances vector is _huge_ (easily up to 50k items) whereas only 1-2 items will actually overlap // with the cursor. --> Make this loop more compact by putting as much as possible into a function call. - const auto added = _drawCursorForegroundSlowPath(p, c, i); + const auto added = _drawCursorForegroundSlowPath(c, i); i += added; instancesCount += added; } @@ -1787,7 +1798,7 @@ void BackendD3D::_drawCursorForeground(const RenderingPayload& p) } } -size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset) +size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t offset) { // We won't die from copying 24 bytes. It simplifies the code below especially in // respect to when/if we overwrite the _instances[offset] slot with a cutout. @@ -1886,7 +1897,9 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, cons target.color = it.color; } - const auto cursorColor = p.s->cursor->cursorColor; + auto color = c.foreground == 0xffffffff ? it.color ^ 0xffffff : c.foreground; + color = ColorFix::GetPerceivableColor(color, c.background, 0.5f * 0.5f); + // If the cursor covers the entire glyph (like, let's say, a full-box cursor with an ASCII character), // we don't append a new quad, but rather reuse the one that already exists (cutoutCount == 0). auto& target = cutoutCount ? _appendQuad() : _instances[offset]; @@ -1898,7 +1911,7 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, cons target.size.y = static_cast(intersectionB - intersectionT); target.texcoord.x = static_cast(it.texcoord.x + intersectionL - instanceL); target.texcoord.y = static_cast(it.texcoord.y + intersectionT - instanceT); - target.color = cursorColor == 0xffffffff ? it.color ^ 0xc0c0c0 : c.foreground; + target.color = color; return addedInstances; } diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index c5cf07e655..f75ae95407 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -220,8 +220,8 @@ namespace Microsoft::Console::Render::Atlas void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); void _drawGridlines(const RenderingPayload& p, u16 y); void _drawCursorBackground(const RenderingPayload& p); - ATLAS_ATTR_COLD void _drawCursorForeground(const RenderingPayload& p); - ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset); + ATLAS_ATTR_COLD void _drawCursorForeground(); + ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const CursorRect& c, size_t offset); void _drawSelection(const RenderingPayload& p); void _executeCustomShader(RenderingPayload& p); diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp index 65a63a707d..03c4795635 100644 --- a/src/renderer/base/RenderSettings.cpp +++ b/src/renderer/base/RenderSettings.cpp @@ -11,10 +11,6 @@ using namespace Microsoft::Console::Render; using Microsoft::Console::Utils::InitializeColorTable; -static constexpr size_t AdjustedFgIndex{ 16 }; -static constexpr size_t AdjustedBgIndex{ 17 }; -static constexpr size_t AdjustedBrightFgIndex{ 18 }; - RenderSettings::RenderSettings() noexcept { InitializeColorTable(_colorTable); @@ -71,42 +67,6 @@ void RenderSettings::ResetColorTable() noexcept InitializeColorTable({ _colorTable.data(), 16 }); } -// Routine Description: -// - Creates the adjusted color array, which contains the possible foreground colors, -// adjusted for perceivability -// - The adjusted color array is 2-d, and effectively maps a background and foreground -// color pair to the adjusted foreground for that color pair -void RenderSettings::MakeAdjustedColorArray() noexcept -{ - // The color table has 16 colors, but the adjusted color table needs to be 19 - // to include the default background, default foreground and bright default foreground colors - std::array colorTableWithDefaults; - std::copy_n(std::begin(_colorTable), 16, std::begin(colorTableWithDefaults)); - colorTableWithDefaults[AdjustedFgIndex] = GetColorAlias(ColorAlias::DefaultForeground); - colorTableWithDefaults[AdjustedBgIndex] = GetColorAlias(ColorAlias::DefaultBackground); - - // We need to use TextColor to calculate the bright default fg - TextColor defaultFg; - colorTableWithDefaults[AdjustedBrightFgIndex] = defaultFg.GetColor(_colorTable, GetColorAliasIndex(ColorAlias::DefaultForeground), true); - - for (auto fgIndex = 0; fgIndex < 19; ++fgIndex) - { - const auto fg = til::at(colorTableWithDefaults, fgIndex); - for (auto bgIndex = 0; bgIndex < 19; ++bgIndex) - { - if (fgIndex == bgIndex) - { - _adjustedForegroundColors[bgIndex][fgIndex] = fg; - } - else - { - const auto bg = til::at(colorTableWithDefaults, bgIndex); - _adjustedForegroundColors[bgIndex][fgIndex] = ColorFix::GetPerceivableColor(fg, bg); - } - } - } -} - // Routine Description: // - Updates the given index in the color table to a new value. // Arguments: @@ -196,74 +156,37 @@ std::pair RenderSettings::GetAttributeColors(const TextAttri const auto dimFg = attr.IsFaint() || (_blinkShouldBeFaint && attr.IsBlinking()); const auto swapFgAndBg = attr.IsReverseVideo() ^ GetRenderMode(Mode::ScreenReversed); - // We want to nudge the foreground color to make it more perceivable only for the - // default color pairs within the color table - if (Feature_AdjustIndistinguishableText::IsEnabled() && - GetRenderMode(Mode::IndexedDistinguishableColors) && - !dimFg && - !attr.IsInvisible() && - (fgTextColor.IsDefault() || fgTextColor.IsLegacy()) && - (bgTextColor.IsDefault() || bgTextColor.IsLegacy())) + auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); + auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); + + if (dimFg) { - const auto bgIndex = bgTextColor.IsDefault() ? AdjustedBgIndex : bgTextColor.GetIndex(); - auto fgIndex = fgTextColor.IsDefault() ? AdjustedFgIndex : fgTextColor.GetIndex(); - - if (brightenFg) - { - // There is a special case for intense here - we need to get the bright version of the foreground color - if (fgTextColor.IsIndex16() && (fgIndex < 8)) - { - fgIndex += 8; - } - else if (fgTextColor.IsDefault()) - { - fgIndex = AdjustedBrightFgIndex; - } - } - - if (swapFgAndBg) - { - const auto fg = _adjustedForegroundColors[fgIndex][bgIndex]; - const auto bg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); - return { fg, bg }; - } - else - { - const auto fg = _adjustedForegroundColors[bgIndex][fgIndex]; - const auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); - return { fg, bg }; - } + fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. } - else + if (swapFgAndBg) { - auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); - auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); + std::swap(fg, bg); + } + if (attr.IsInvisible()) + { + fg = bg; + } - if (dimFg) - { - fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. - } - if (swapFgAndBg) - { - std::swap(fg, bg); - } - if (attr.IsInvisible()) - { - fg = bg; - } - - // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to - // catch the cases where the fg was intentionally set to be the same as the bg. In either case, - // don't adjust the foreground. - if (Feature_AdjustIndistinguishableText::IsEnabled() && + // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to + // catch the cases where the fg was intentionally set to be the same as the bg. In either case, + // don't adjust the foreground. + if constexpr (Feature_AdjustIndistinguishableText::IsEnabled()) + { + if ( + _renderMode.any(Mode::IndexedDistinguishableColors, Mode::AlwaysDistinguishableColors) && fg != bg && - GetRenderMode(Mode::AlwaysDistinguishableColors)) + (_renderMode.test(Mode::AlwaysDistinguishableColors) || (fgTextColor.IsDefaultOrLegacy() && bgTextColor.IsDefaultOrLegacy()))) { - fg = ColorFix::GetPerceivableColor(fg, bg); + fg = ColorFix::GetPerceivableColor(fg, bg, 0.5f * 0.5f); } - - return { fg, bg }; } + + return { fg, bg }; } // Routine Description: diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp index 897d8e099d..4b6e7c3981 100644 --- a/src/renderer/inc/RenderSettings.hpp +++ b/src/renderer/inc/RenderSettings.hpp @@ -33,7 +33,6 @@ namespace Microsoft::Console::Render bool GetRenderMode(const Mode mode) const noexcept; const std::array& GetColorTable() const noexcept; void ResetColorTable() noexcept; - void MakeAdjustedColorArray() noexcept; void SetColorTableEntry(const size_t tableIndex, const COLORREF color); COLORREF GetColorTableEntry(const size_t tableIndex) const; void SetColorAlias(const ColorAlias alias, const size_t tableIndex, const COLORREF color); @@ -48,7 +47,6 @@ namespace Microsoft::Console::Render til::enumset _renderMode{ Mode::BlinkAllowed, Mode::IntenseIsBright }; std::array _colorTable; std::array(ColorAlias::ENUM_COUNT)> _colorAliasIndices; - std::array, 19> _adjustedForegroundColors; size_t _blinkCycle = 0; mutable bool _blinkIsInUse = false; bool _blinkShouldBeFaint = false; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 6a4d7b8d67..7c2c04705f 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2940,11 +2940,6 @@ bool AdaptDispatch::SetClipboard(const std::wstring_view content) bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) { _renderSettings.SetColorTableEntry(tableIndex, dwColor); - if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors)) - { - // Re-calculate the adjusted colors now that one of the entries has been changed - _renderSettings.MakeAdjustedColorArray(); - } // If we're a conpty, always return false, so that we send the updated color // value to the terminal. Still handle the sequence so apps that use @@ -3010,11 +3005,6 @@ bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt case DispatchTypes::ColorItem::NormalText: _renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, fgIndex); _renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, bgIndex); - if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors)) - { - // Re-calculate the adjusted colors now that these aliases have been changed - _renderSettings.MakeAdjustedColorArray(); - } break; case DispatchTypes::ColorItem::WindowFrame: _renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, fgIndex); diff --git a/src/types/ColorFix.cpp b/src/types/ColorFix.cpp index 723d7059f6..eda47d396a 100644 --- a/src/types/ColorFix.cpp +++ b/src/types/ColorFix.cpp @@ -1,231 +1,212 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// A lot of code was taken from -// https://github.com/Maximus5/ConEmu/blob/master/src/ConEmu/ColorFix.cpp -// and then adjusted to fit our style guidelines - #include "precomp.h" - #include "inc/ColorFix.hpp" +// This table contains a direct mapping from 8-bit sRGB to linear RGB. +// It was generated using the following code: +// #include +// +// int main() { +// char buffer[32]; +// +// for (int i = 0; i < 256; ++i) { +// auto v = i / 255.0f; +// v = v <= 0.04045f ? v / 12.92f : powf((v + 0.055f) / 1.055f, 2.4f); +// +// auto p = std::to_chars(&buffer[0], &buffer[32], v, std::chars_format::hex, -1).ptr; +// strcpy_s(p, &buffer[32] - p, "f,"); +// +// if (i % 16 == 0) printf("\n "); +// printf("0x%-15s", &buffer[0]); +// } +// +// return 0; +// } +// +// clang-format off +static constexpr float srgbToRgbLUT[256]{ + 0x0.000000p+0f, 0x1.3e4568p-12f, 0x1.3e4568p-11f, 0x1.dd681cp-11f, 0x1.3e4568p-10f, 0x1.8dd6c2p-10f, 0x1.dd681cp-10f, 0x1.167cbap-9f, 0x1.3e4568p-9f, 0x1.660e16p-9f, 0x1.8dd6c2p-9f, 0x1.b6a31ap-9f, 0x1.e1e31ap-9f, 0x1.07c38cp-8f, 0x1.1fcc2cp-8f, 0x1.390ffap-8f, + 0x1.53936ep-8f, 0x1.6f5adep-8f, 0x1.8c6a92p-8f, 0x1.aac6c2p-8f, 0x1.ca7382p-8f, 0x1.eb74e0p-8f, 0x1.06e76ap-7f, 0x1.18c2a4p-7f, 0x1.2b4e06p-7f, 0x1.3e8b7cp-7f, 0x1.527cd6p-7f, 0x1.6723eep-7f, 0x1.7c8292p-7f, 0x1.929a86p-7f, 0x1.a96d8ep-7f, 0x1.c0fd62p-7f, + 0x1.d94bbep-7f, 0x1.f25a44p-7f, 0x1.061550p-6f, 0x1.135f3ep-6f, 0x1.210bb6p-6f, 0x1.2f1b8ap-6f, 0x1.3d8f84p-6f, 0x1.4c6866p-6f, 0x1.5ba6fap-6f, 0x1.6b4c02p-6f, 0x1.7b5840p-6f, 0x1.8bcc72p-6f, 0x1.9ca956p-6f, 0x1.adefaap-6f, 0x1.bfa020p-6f, 0x1.d1bb72p-6f, + 0x1.e44258p-6f, 0x1.f73582p-6f, 0x1.054ad2p-5f, 0x1.0f31b8p-5f, 0x1.194fccp-5f, 0x1.23a55ep-5f, 0x1.2e32c6p-5f, 0x1.38f85cp-5f, 0x1.43f678p-5f, 0x1.4f2d64p-5f, 0x1.5a9d76p-5f, 0x1.664700p-5f, 0x1.722a56p-5f, 0x1.7e47c6p-5f, 0x1.8a9fa2p-5f, 0x1.973238p-5f, + 0x1.a3ffdep-5f, 0x1.b108d4p-5f, 0x1.be4d6ep-5f, 0x1.cbcdfcp-5f, 0x1.d98ac8p-5f, 0x1.e7841ep-5f, 0x1.f5ba52p-5f, 0x1.0216cep-4f, 0x1.096f2ap-4f, 0x1.10e660p-4f, 0x1.187c94p-4f, 0x1.2031ecp-4f, 0x1.28068ap-4f, 0x1.2ffa94p-4f, 0x1.380e2cp-4f, 0x1.404176p-4f, + 0x1.489496p-4f, 0x1.5107aep-4f, 0x1.599ae0p-4f, 0x1.624e50p-4f, 0x1.6b2224p-4f, 0x1.741676p-4f, 0x1.7d2b6ap-4f, 0x1.866124p-4f, 0x1.8fb7c4p-4f, 0x1.992f6cp-4f, 0x1.a2c83ep-4f, 0x1.ac8258p-4f, 0x1.b65ddep-4f, 0x1.c05aeep-4f, 0x1.ca79aap-4f, 0x1.d4ba32p-4f, + 0x1.df1ca4p-4f, 0x1.e9a122p-4f, 0x1.f447d0p-4f, 0x1.ff10c2p-4f, 0x1.04fe0ep-3f, 0x1.0a8500p-3f, 0x1.101d46p-3f, 0x1.15c6f0p-3f, 0x1.1b820ap-3f, 0x1.214ea8p-3f, 0x1.272cd6p-3f, 0x1.2d1ca4p-3f, 0x1.331e20p-3f, 0x1.39315ap-3f, 0x1.3f5660p-3f, 0x1.458d46p-3f, + 0x1.4bd612p-3f, 0x1.5230d8p-3f, 0x1.589da0p-3f, 0x1.5f1c82p-3f, 0x1.65ad88p-3f, 0x1.6c50c2p-3f, 0x1.73063cp-3f, 0x1.79ce06p-3f, 0x1.80a82cp-3f, 0x1.8794bep-3f, 0x1.8e93c8p-3f, 0x1.95a55cp-3f, 0x1.9cc984p-3f, 0x1.a4004ep-3f, 0x1.ab49ccp-3f, 0x1.b2a606p-3f, + 0x1.ba1516p-3f, 0x1.c196f8p-3f, 0x1.c92bc0p-3f, 0x1.d0d37ep-3f, 0x1.d88e40p-3f, 0x1.e05c18p-3f, 0x1.e83d08p-3f, 0x1.f03120p-3f, 0x1.f83870p-3f, 0x1.002984p-2f, 0x1.044078p-2f, 0x1.08611ap-2f, 0x1.0c8b74p-2f, 0x1.10bf8ap-2f, 0x1.14fd64p-2f, 0x1.194506p-2f, + 0x1.1d967ap-2f, 0x1.21f1c2p-2f, 0x1.2656e8p-2f, 0x1.2ac5f0p-2f, 0x1.2f3ee2p-2f, 0x1.33c1c4p-2f, 0x1.384e9cp-2f, 0x1.3ce56ep-2f, 0x1.418644p-2f, 0x1.463124p-2f, 0x1.4ae610p-2f, 0x1.4fa512p-2f, 0x1.546e30p-2f, 0x1.59416ep-2f, 0x1.5e1ed2p-2f, 0x1.630666p-2f, + 0x1.67f830p-2f, 0x1.6cf430p-2f, 0x1.71fa6ep-2f, 0x1.770af2p-2f, 0x1.7c25c2p-2f, 0x1.814ae2p-2f, 0x1.867a5ap-2f, 0x1.8bb42ep-2f, 0x1.90f866p-2f, 0x1.964706p-2f, 0x1.9ba016p-2f, 0x1.a1039ap-2f, 0x1.a67198p-2f, 0x1.abea16p-2f, 0x1.b16d1ap-2f, 0x1.b6faa8p-2f, + 0x1.bc92cap-2f, 0x1.c23582p-2f, 0x1.c7e2d6p-2f, 0x1.cd9acep-2f, 0x1.d35d6cp-2f, 0x1.d92abap-2f, 0x1.df02bap-2f, 0x1.e4e574p-2f, 0x1.ead2ecp-2f, 0x1.f0cb28p-2f, 0x1.f6ce2ep-2f, 0x1.fcdc04p-2f, 0x1.017a5ap-1f, 0x1.048c1cp-1f, 0x1.07a34ep-1f, 0x1.0abff2p-1f, + 0x1.0de20cp-1f, 0x1.11099ep-1f, 0x1.1436acp-1f, 0x1.176936p-1f, 0x1.1aa140p-1f, 0x1.1ddecep-1f, 0x1.2121e2p-1f, 0x1.246a7cp-1f, 0x1.27b8a2p-1f, 0x1.2b0c56p-1f, 0x1.2e659ap-1f, 0x1.31c472p-1f, 0x1.3528dep-1f, 0x1.3892e2p-1f, 0x1.3c0282p-1f, 0x1.3f77bep-1f, + 0x1.42f29cp-1f, 0x1.46731cp-1f, 0x1.49f940p-1f, 0x1.4d850cp-1f, 0x1.511682p-1f, 0x1.54ada6p-1f, 0x1.584a7ap-1f, 0x1.5bed00p-1f, 0x1.5f953cp-1f, 0x1.63432ep-1f, 0x1.66f6d8p-1f, 0x1.6ab040p-1f, 0x1.6e6f66p-1f, 0x1.72344cp-1f, 0x1.75fef8p-1f, 0x1.79cf6ap-1f, + 0x1.7da5a4p-1f, 0x1.8181a8p-1f, 0x1.85637cp-1f, 0x1.894b20p-1f, 0x1.8d3896p-1f, 0x1.912be2p-1f, 0x1.952504p-1f, 0x1.992402p-1f, 0x1.9d28dcp-1f, 0x1.a13396p-1f, 0x1.a54430p-1f, 0x1.a95aaep-1f, 0x1.ad7714p-1f, 0x1.b19960p-1f, 0x1.b5c19ap-1f, 0x1.b9efc0p-1f, + 0x1.be23d8p-1f, 0x1.c25ddcp-1f, 0x1.c69ddep-1f, 0x1.cae3d2p-1f, 0x1.cf2fc6p-1f, 0x1.d381acp-1f, 0x1.d7d99ap-1f, 0x1.dc377ep-1f, 0x1.e09b70p-1f, 0x1.e5055ep-1f, 0x1.e9755cp-1f, 0x1.edeb5cp-1f, 0x1.f26772p-1f, 0x1.f6e98cp-1f, 0x1.fb71c0p-1f, 0x1.000000p+0f, +}; +// clang-format on + TIL_FAST_MATH_BEGIN -static constexpr float gMinThreshold = 12.0f; -static constexpr float gExpThreshold = 20.0f; -static constexpr float gLStep = 5.0f; - -static constexpr float rad006 = 0.104719755119659774f; -static constexpr float rad025 = 0.436332312998582394f; -static constexpr float rad030 = 0.523598775598298873f; -static constexpr float rad060 = 1.047197551196597746f; -static constexpr float rad063 = 1.099557428756427633f; -static constexpr float rad180 = 3.141592653589793238f; -static constexpr float rad275 = 4.799655442984406336f; -static constexpr float rad360 = 6.283185307179586476f; - -ColorFix::ColorFix(COLORREF color) noexcept +constexpr float saturate(float f) noexcept { - rgb = color; - _ToLab(); + return f < 0 ? 0 : (f > 1 ? 1 : f); } -// Method Description: -// - Helper function to calculate HPrime -float ColorFix::_GetHPrimeFn(float x, float y) noexcept +// FP32 is the very epitome of defined behavior. +#pragma warning(suppress : 26497) // You can attempt to make 'cbrtf_est' constexpr unless it contains any undefined behavior (f.4).) +__forceinline float cbrtf_est(float a) noexcept { - if (x == 0 && y == 0) + // We can't use std::bit_cast here, even though it exists exactly for this purpose. + // MSVC is dumb and introduces function calls to std::bit_cast + // even at the highest optimization and inlining levels. What. + union FP32 { - return 0; + uint32_t u; + float f; + }; + + // http://metamerist.com/cbrt/cbrt.htm showed a great estimator for the cube root: + // float_as_uint32_t / 3 + 709921077 + // It's similar to the well known "fast inverse square root" trick. Lots of numbers around 709921077 perform + // at least equally well to 709921077, and it is unknown how and why 709921077 was chosen specifically. + FP32 fp{ .f = a }; // evil floating point bit level hacking + fp.u = fp.u / 3 + 709921077; // what the fuck? + const auto x = fp.f; + + // One round of Newton's method. It follows the Wikipedia article at + // https://en.wikipedia.org/wiki/Cube_root#Numerical_methods + // For `a`s in the range between 0 and 1, this results in a maximum error of + // less than 6.7e-4f, which is not good, but good enough for us, because + // we're not an image editor. The benefit is that it's really fast. + return (1.0f / 3.0f) * (a / (x * x) + (x + x)); // 1st iteration +} + +// This namespace contains functions and structures as defined by: https://bottosson.github.io/posts/oklab/ +// It has been released into the public domain and is also available under an MIT license. +// The only change I made is to replace cbrtf() with cbrtf_est() to cut the CPU cost by a third. +namespace oklab +{ + struct Lab + { + float l; + float a; + float b; + }; + + struct RGB + { + float r; + float g; + float b; + }; + + __forceinline Lab linear_srgb_to_oklab(const RGB& c) noexcept + { + const auto l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b; + const auto m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b; + const auto s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b; + + const auto l_ = cbrtf_est(l); + const auto m_ = cbrtf_est(m); + const auto s_ = cbrtf_est(s); + + return { + 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_, + 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_, + 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_, + }; } - const auto hueAngle = atan2(x, y); - return hueAngle >= 0 ? hueAngle : hueAngle + rad360; -} - -// Method Description: -// - Given 2 colors, computes the DeltaE value between them -// Arguments: -// - x1: the first color -// - x2: the second color -// Return Value: -// - The DeltaE value between x1 and x2 -float ColorFix::_GetDeltaE(ColorFix x1, ColorFix x2) noexcept -{ - constexpr float kSubL = 1; - constexpr float kSubC = 1; - constexpr float kSubH = 1; - - // Delta L Prime - const auto deltaLPrime = x2.L - x1.L; - - // L Bar - const auto lBar = (x1.L + x2.L) / 2; - - // C1 & C2 - const auto c1 = sqrtf(powf(x1.A, 2) + powf(x1.B, 2)); - const auto c2 = sqrtf(powf(x2.A, 2) + powf(x2.B, 2)); - - // C Bar - const auto cBar = (c1 + c2) / 2; - - // A Prime 1 - const auto aPrime1 = x1.A + (x1.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7)))); - - // A Prime 2 - const auto aPrime2 = x2.A + (x2.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7)))); - - // C Prime 1 - const auto cPrime1 = sqrtf(powf(aPrime1, 2) + powf(x1.B, 2)); - - // C Prime 2 - const auto cPrime2 = sqrtf(powf(aPrime2, 2) + powf(x2.B, 2)); - - // C Bar Prime - const auto cBarPrime = (cPrime1 + cPrime2) / 2; - - // Delta C Prime - const auto deltaCPrime = cPrime2 - cPrime1; - - // S sub L - const auto sSubL = 1 + ((0.015f * powf(lBar - 50, 2)) / sqrtf(20 + powf(lBar - 50, 2))); - - // S sub C - const auto sSubC = 1 + 0.045f * cBarPrime; - - // h Prime 1 - const auto hPrime1 = _GetHPrimeFn(x1.B, aPrime1); - - // h Prime 2 - const auto hPrime2 = _GetHPrimeFn(x2.B, aPrime2); - - // Delta H Prime - const auto deltaHPrime = 0 == c1 || 0 == c2 ? 0 : 2 * sqrtf(cPrime1 * cPrime2) * sin(abs(hPrime1 - hPrime2) <= rad180 ? hPrime2 - hPrime1 : (hPrime2 <= hPrime1 ? hPrime2 - hPrime1 + rad360 : hPrime2 - hPrime1 - rad360) / 2); - - // H Bar Prime - const auto hBarPrime = (abs(hPrime1 - hPrime2) > rad180) ? (hPrime1 + hPrime2 + rad360) / 2 : (hPrime1 + hPrime2) / 2; - - // T - const auto t = 1 - 0.17f * cosf(hBarPrime - rad030) + 0.24f * cosf(2 * hBarPrime) + 0.32f * cosf(3 * hBarPrime + rad006) - 0.20f * cosf(4 * hBarPrime - rad063); - - // S sub H - const auto sSubH = 1 + 0.015f * cBarPrime * t; - - // R sub T - const auto rSubT = -2 * sqrtf(powf(cBarPrime, 7) / (powf(cBarPrime, 7) + powf(25.0f, 7))) * sin(rad060 * exp(-powf((hBarPrime - rad275) / rad025, 2))); - - // Put it all together! - const auto lightness = deltaLPrime / (kSubL * sSubL); - const auto chroma = deltaCPrime / (kSubC * sSubC); - const auto hue = deltaHPrime / (kSubH * sSubH); - - return sqrtf(powf(lightness, 2) + powf(chroma, 2) + powf(hue, 2) + rSubT * chroma * hue); -} - -// Method Description: -// - Populates our L, A, B values, based on our r, g, b values -// - Converts a color in rgb format to a color in lab format -// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1 -void ColorFix::_ToLab() noexcept -{ - auto var_R = r / 255.0f; - auto var_G = g / 255.0f; - auto var_B = b / 255.0f; - - var_R = var_R > 0.04045f ? powf(((var_R + 0.055f) / 1.055f), 2.4f) : var_R / 12.92f; - var_G = var_G > 0.04045f ? powf(((var_G + 0.055f) / 1.055f), 2.4f) : var_G / 12.92f; - var_B = var_B > 0.04045f ? powf(((var_B + 0.055f) / 1.055f), 2.4f) : var_B / 12.92f; - - var_R = var_R * 100.0f; - var_G = var_G * 100.0f; - var_B = var_B * 100.0f; - - //Observer. = 2 degrees, Illuminant = D65 - const auto X = var_R * 0.4124f + var_G * 0.3576f + var_B * 0.1805f; - const auto Y = var_R * 0.2126f + var_G * 0.7152f + var_B * 0.0722f; - const auto Z = var_R * 0.0193f + var_G * 0.1192f + var_B * 0.9505f; - - auto var_X = X / 95.047f; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65) - auto var_Y = Y / 100.000f; //ref_Y = 100.000 - auto var_Z = Z / 108.883f; //ref_Z = 108.883 - - var_X = var_X > 0.008856f ? powf(var_X, (1.0f / 3.0f)) : (7.787f * var_X) + (16.0f / 116.0f); - var_Y = var_Y > 0.008856f ? powf(var_Y, (1.0f / 3.0f)) : (7.787f * var_Y) + (16.0f / 116.0f); - var_Z = var_Z > 0.008856f ? powf(var_Z, (1.0f / 3.0f)) : (7.787f * var_Z) + (16.0f / 116.0f); - - L = (116.0f * var_Y) - 16.0f; - A = 500.0f * (var_X - var_Y); - B = 200.0f * (var_Y - var_Z); -} - -// Method Description: -// - Populates our r, g, b values, based on our L, A, B values -// - Converts a color in lab format to a color in rgb format -// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1 -void ColorFix::_ToRGB() -{ - auto var_Y = (L + 16.0f) / 116.0f; - auto var_X = A / 500.0f + var_Y; - auto var_Z = var_Y - B / 200.0f; - - var_Y = (powf(var_Y, 3) > 0.008856f) ? powf(var_Y, 3) : (var_Y - 16.0f / 116.0f) / 7.787f; - var_X = (powf(var_X, 3) > 0.008856f) ? powf(var_X, 3) : (var_X - 16.0f / 116.0f) / 7.787f; - var_Z = (powf(var_Z, 3) > 0.008856f) ? powf(var_Z, 3) : (var_Z - 16.0f / 116.0f) / 7.787f; - - const auto X = 95.047f * var_X; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65) - const auto Y = 100.000f * var_Y; //ref_Y = 100.000 - const auto Z = 108.883f * var_Z; //ref_Z = 108.883 - - var_X = X / 100.0f; //X from 0 to 95.047 (Observer = 2 degrees, Illuminant = D65) - var_Y = Y / 100.0f; //Y from 0 to 100.000 - var_Z = Z / 100.0f; //Z from 0 to 108.883 - - auto var_R = var_X * 3.2406f + var_Y * -1.5372f + var_Z * -0.4986f; - auto var_G = var_X * -0.9689f + var_Y * 1.8758f + var_Z * 0.0415f; - auto var_B = var_X * 0.0557f + var_Y * -0.2040f + var_Z * 1.0570f; - - var_R = var_R > 0.0031308f ? 1.055f * powf(var_R, (1 / 2.4f)) - 0.055f : var_R = 12.92f * var_R; - var_G = var_G > 0.0031308f ? 1.055f * powf(var_G, (1 / 2.4f)) - 0.055f : var_G = 12.92f * var_G; - var_B = var_B > 0.0031308f ? 1.055f * powf(var_B, (1 / 2.4f)) - 0.055f : var_B = 12.92f * var_B; - - r = (BYTE)std::clamp(var_R * 255.0f, 0.0f, 255.0f); - g = (BYTE)std::clamp(var_G * 255.0f, 0.0f, 255.0f); - b = (BYTE)std::clamp(var_B * 255.0f, 0.0f, 255.0f); -} - -// Method Description: -// - Given foreground and background colors, change the foreground color to -// make it more perceivable if necessary -// - Arguments: -// - fg: the foreground color -// - bg: the background color -// - Return Value: -// - The foreground color after performing any necessary changes to make it more perceivable -COLORREF ColorFix::GetPerceivableColor(COLORREF fg, COLORREF bg) -{ - const ColorFix backLab(bg); - ColorFix frontLab(fg); - const auto de1 = _GetDeltaE(frontLab, backLab); - if (de1 < gMinThreshold) + __forceinline RGB oklab_to_linear_srgb(const Lab& c) noexcept { - for (auto i = 0; i <= 1; i++) - { - const auto step = (i == 0) ? gLStep : -gLStep; - frontLab.L += step; + const auto l_ = c.l + 0.3963377774f * c.a + 0.2158037573f * c.b; + const auto m_ = c.l - 0.1055613458f * c.a - 0.0638541728f * c.b; + const auto s_ = c.l - 0.0894841775f * c.a - 1.2914855480f * c.b; - while (((i == 0) && (frontLab.L <= 100)) || ((i == 1) && (frontLab.L >= 0))) - { - const auto de2 = _GetDeltaE(frontLab, backLab); - if (de2 >= gExpThreshold) - { - frontLab._ToRGB(); - return frontLab.rgb; - } - frontLab.L += step; - } - } + const auto l = l_ * l_ * l_; + const auto m = m_ * m_ * m_; + const auto s = s_ * s_ * s_; + + return { + +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s, + -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s, + -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s, + }; } - return frontLab.rgb; +} + +#pragma warning(push) +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). + +__forceinline oklab::RGB colorrefToLinear(COLORREF c) noexcept +{ + const auto r = srgbToRgbLUT[(c >> 0) & 0xff]; + const auto g = srgbToRgbLUT[(c >> 8) & 0xff]; + const auto b = srgbToRgbLUT[(c >> 16) & 0xff]; + return { r, g, b }; +} + +#pragma warning(pop) + +__forceinline COLORREF linearToColorref(const oklab::RGB& c) noexcept +{ + auto r = saturate(c.r); + auto g = saturate(c.g); + auto b = saturate(c.b); + r = (r > 0.0031308f ? (1.055f * powf(r, 1.0f / 2.4f) - 0.055f) : 12.92f * r) * 255.0f; + g = (g > 0.0031308f ? (1.055f * powf(g, 1.0f / 2.4f) - 0.055f) : 12.92f * g) * 255.0f; + b = (b > 0.0031308f ? (1.055f * powf(b, 1.0f / 2.4f) - 0.055f) : 12.92f * b) * 255.0f; + return lrintf(r) | (lrintf(g) << 8) | (lrintf(b) << 16); +} + +// This function changes `color` so that it is visually different +// enough from `reference` that it's (much more easily) readable. +// See /doc/color_nudging.html +COLORREF ColorFix::GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept +{ + const auto referenceOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(reference)); + auto colorOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(color)); + + // To determine whether the two colors are too close to each other we use the ΔEOK metric + // based on the Oklab color space. It's defined as the simple euclidean distance between. + auto dl = referenceOklab.l - colorOklab.l; + auto da = referenceOklab.a - colorOklab.a; + auto db = referenceOklab.b - colorOklab.b; + dl *= dl; + da *= da; + db *= db; + + const auto distance = dl + da + db; + if (distance >= minSquaredDistance) + { + return color; + } + + // Thanks to ΔEOK being the euclidean distance we can immediately compute the + // minimum .l distance that's required for `distance` to be >= `minSquaredDistance`. + auto deltaL = sqrtf(minSquaredDistance - da - db); + + // Try to retain the brightness relationship between `reference` and `color`. + // If `color` is darker than `reference`, we should first try to make it even darker. + if (colorOklab.l < referenceOklab.l) + { + deltaL = -deltaL; + } + + // This primitive way of adjusting the lightness will result in gamut clipping. That's really not good + // (it reduces the contrast significantly for some colors), but on the other hand, doing proper gamut + // mapping is annoying and expensive. I was unable to find an easy and cheap (!) algorithm that would + // change the chroma so that we're (mostly) back inside the gamut, but found none. I'm sure it's + // possible to come up with something, but I left that as a future improvement. + colorOklab.l = referenceOklab.l + deltaL; + if (colorOklab.l < 0 || colorOklab.l > 1) + { + colorOklab.l = referenceOklab.l - deltaL; + } + + return linearToColorref(oklab::oklab_to_linear_srgb(colorOklab)) | (color & 0xff000000); } TIL_FAST_MATH_END diff --git a/src/types/inc/ColorFix.hpp b/src/types/inc/ColorFix.hpp index a233d1268a..02bdc0631b 100644 --- a/src/types/inc/ColorFix.hpp +++ b/src/types/inc/ColorFix.hpp @@ -1,53 +1,13 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- ColorFix - -Abstract: -- Implementation of perceptual color nudging, which allows the Terminal - to slightly shift the foreground color to make it more perceivable on - the current background (for cases where the foreground is very close - to being imperceivable on the background). - -Author(s): -- Pankaj Bhojwani - Sep 2021 - ---*/ - +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. +// +// Implementation of perceptual color nudging, which allows the Terminal +// to slightly shift the foreground color to make it more perceivable on +// the current background (for cases where the foreground is very close +// to being imperceivable on the background). #pragma once -struct ColorFix +namespace ColorFix { -public: - ColorFix(COLORREF color) noexcept; - - static COLORREF GetPerceivableColor(COLORREF fg, COLORREF bg); - -#pragma warning(push) - // CL will complain about the both nameless and anonymous struct. -#pragma warning(disable : 4201) - // RGB - union - { - struct - { - BYTE r, g, b, dummy; - }; - COLORREF rgb; - }; - - // Lab - struct - { - float L, A, B; - }; -#pragma warning(pop) - -private: - static float _GetHPrimeFn(float x, float y) noexcept; - static float _GetDeltaE(ColorFix x1, ColorFix x2) noexcept; - void _ToLab() noexcept; - void _ToRGB(); -}; + COLORREF GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept; +}