From dc29be39a640e9300204f5f1ec676e7b09a2fa08 Mon Sep 17 00:00:00 2001 From: Mariusz Glebocki Date: Sat, 17 Aug 2019 16:00:45 -0400 Subject: [PATCH] Keep perceived contrast in random background color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Randomize colors using HSLuv color space instead of HSL. It has perceptually uniform lightness, which means every color with the same lightness value is perceived as equally bright by humans. Displays quality and lack of calibration in most monitors lowers this uniformity, but even on worst color display it should be better than standard HSL. More information about HSLuv: http://www.hsluv.org/ Minor changes: * Random seed takes PID into account to prevent repeated colors in separate Konsole processes * Key names in a config were changed * Adapted "Black on random light" color scheme Breeze with random ranges on default bg and fg: hue=360° saturation=100: {F6754773} To be done in future: * Automatically convert color schemes which use old randomization method. Reviewers: #konsole, #vdg Subscribers: hindenburg, #vdg, #konsole Tags: #konsole, #vdg Differential Revision: https://phabricator.kde.org/D20263 --- .../BlackOnRandomLight.colorscheme | 11 +- src/CMakeLists.txt | 1 + src/CharacterColor.h | 35 ++ src/ColorScheme.cpp | 212 +++++--- src/ColorScheme.h | 38 +- src/ColorSchemeEditor.cpp | 4 +- src/ColorSchemeEditor.ui | 6 +- src/ViewManager.cpp | 3 +- src/hsluv.c | 453 ++++++++++++++++++ src/hsluv.h | 90 ++++ 10 files changed, 756 insertions(+), 97 deletions(-) create mode 100644 src/hsluv.c create mode 100644 src/hsluv.h diff --git a/data/color-schemes/BlackOnRandomLight.colorscheme b/data/color-schemes/BlackOnRandomLight.colorscheme index 0a48d95e5..ddfbf1376 100644 --- a/data/color-schemes/BlackOnRandomLight.colorscheme +++ b/data/color-schemes/BlackOnRandomLight.colorscheme @@ -1,12 +1,20 @@ [Background] Color=247,247,214 -MaxRandomHue=340 +RandomHueRange=360 +RandomSaturationRange=25 +RandomLightnessRange=10 [BackgroundIntense] Color=255,255,221 +RandomHueRange=360 +RandomSaturationRange=25 +RandomLightnessRange=10 [BackgroundFaint] Color=247,247,214 +RandomHueRange=360 +RandomSaturationRange=25 +RandomLightnessRange=10 [Color0] Color=0,0,0 @@ -92,4 +100,5 @@ Color=0,0,0 [General] Description=Black on Random Light +ColorRandomization=true Opacity=1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 87efe8c06..bbdb6c074 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,7 @@ qt5_add_dbus_adaptor(windowadaptors_SRCS set(konsoleprivate_SRCS ${sessionadaptors_SRCS} ${windowadaptors_SRCS} + hsluv.c BookmarkHandler.cpp ColorScheme.cpp ColorSchemeManager.cpp diff --git a/src/CharacterColor.h b/src/CharacterColor.h index 83d5f6e7e..380f96ab6 100644 --- a/src/CharacterColor.h +++ b/src/CharacterColor.h @@ -43,6 +43,41 @@ typedef QColor ColorEntry; #define INTENSITIES 3 #define TABLE_COLORS (INTENSITIES*BASE_COLORS) +enum ColorTableIndex { + ColorFgIndex, + ColorBgIndex, + Color0Index, + Color1Index, + Color2Index, + Color3Index, + Color4Index, + Color5Index, + Color6Index, + Color7Index, + + ColorFgIntenseIndex, + ColorBgIntenseIndex, + Color0IntenseIndex, + Color1IntenseIndex, + Color2IntenseIndex, + Color3IntenseIndex, + Color4IntenseIndex, + Color5IntenseIndex, + Color6IntenseIndex, + Color7IntenseIndex, + + ColorFgFaintIndex, + ColorBgFaintIndex, + Color0FaintIndex, + Color1FaintIndex, + Color2FaintIndex, + Color3FaintIndex, + Color4FaintIndex, + Color5FaintIndex, + Color6FaintIndex, + Color7FaintIndex, +}; + #define DEFAULT_FORE_COLOR 0 #define DEFAULT_BACK_COLOR 1 diff --git a/src/ColorScheme.cpp b/src/ColorScheme.cpp index 6d460c8f7..8ebc45992 100644 --- a/src/ColorScheme.cpp +++ b/src/ColorScheme.cpp @@ -21,9 +21,11 @@ // Own #include "ColorScheme.h" +#include "hsluv.h" // Qt #include +#include // KDE #include @@ -38,6 +40,15 @@ namespace { const int FGCOLOR_INDEX = 0; const int BGCOLOR_INDEX = 1; + +const char RandomHueRangeKey[] = "RandomHueRange"; +const char RandomSaturationRangeKey[] = "RandomSaturationRange"; +const char RandomLightnessRangeKey[] = "RandomLightnessRange"; +const char EnableColorRandomizationKey[] = "ColorRandomization"; + +const double MaxHue = 360.0; +const double MaxSaturation = 100.0; +const double MaxLightness = 100.0; } using namespace Konsole; @@ -193,7 +204,7 @@ ColorScheme::ColorScheme(const ColorScheme &other) : if (other._randomTable != nullptr) { for (int i = 0; i < TABLE_COLORS; i++) { const RandomizationRange &range = other._randomTable[i]; - setRandomizationRange(i, range.hue, range.saturation, range.value); + setRandomizationRange(i, range.hue, range.saturation, range.lightness); } } } @@ -250,10 +261,15 @@ ColorEntry ColorScheme::colorEntry(int index, uint randomSeed) const ColorEntry entry = colorTable()[index]; - if (randomSeed == 0 || _randomTable == nullptr || _randomTable[index].isNull()) { + if (!_colorRandomization || randomSeed == 0 || _randomTable == nullptr + || _randomTable[index].isNull()) { return entry; } + double baseHue, baseSaturation, baseLightness; + rgb2hsluv(entry.redF(), entry.greenF(), entry.blueF(), + &baseHue, &baseSaturation, &baseLightness); + const RandomizationRange &range = _randomTable[index]; // 32-bit Mersenne Twister @@ -261,33 +277,56 @@ ColorEntry ColorScheme::colorEntry(int index, uint randomSeed) const // minstd_rand0 which always gives us 0 on the first number. std::mt19937 randomEngine(randomSeed); - int hueDifference = 0; - if (range.hue != 0u) { - std::uniform_int_distribution dist(0, range.hue); - hueDifference = dist(randomEngine); + // Use hues located around base color's hue. + // H=0 [|= =] H=128 [ =|= ] H=360 [= =|] + const double minHue = baseHue - range.hue / 2.0; + const double maxHue = baseHue + range.hue / 2.0; + std::uniform_real_distribution<> hueDistribution(minHue, maxHue); + // Hue value is an angle, it wraps after 360°. Adding MAX_HUE + // guarantees that the sum is not negative. + const double hue = fmod(MaxHue + hueDistribution(randomEngine), MaxHue); + + // Saturation is always decreased. With more saturation more + // information about hue is preserved in RGB color space + // (consider red with S=100 and "red" with S=0 which is gray). + // Additionally, I think it can be easier to imagine more + // toned color than more vivid one. + // S=0 [|== ] S=50 [ ==| ] S=100 [ ==|] + const double minSaturation = qMax(baseSaturation - range.saturation, 0.0); + const double maxSaturation = qMax(range.saturation, baseSaturation); + // Use rising linear distribution as colors with lower + // saturation are less distinguishable. + std::piecewise_linear_distribution<> saturationDistribution({minSaturation, maxSaturation}, + [](double v) { return v; }); + const double saturation = qFuzzyCompare(minSaturation, maxSaturation) + ? baseSaturation + : saturationDistribution(randomEngine); + + // Lightness range has base value at its center. The base + // value is clamped to prevent the range from shrinking. + // L=0 [=|= ] L=50 [ =|= ] L=100 [ =|=] + baseLightness = qBound(range.lightness / 2.0, baseLightness , MaxLightness - range.lightness); + const double minLightness = qMax(baseLightness - range.lightness / 2.0, 0.0); + const double maxLightness = qMin(baseLightness + range.lightness / 2.0, MaxLightness); + // Use triangular distribution with peak at L=50.0. + // Dark and very light colors are less distinguishable. + std::initializer_list lightnessIntervals; + if (minLightness < 50.0 && 50.0 < maxLightness) { + lightnessIntervals = {minLightness, 50.0, maxLightness}; + } else { + lightnessIntervals = {minLightness, maxLightness}; } + static const auto lightnessWeightsFunc = [](double v) { return 50.0 - qAbs(v - 50.0); }; + std::piecewise_linear_distribution<> lightnessDistribution(lightnessIntervals, + lightnessWeightsFunc); + const double lightness = qFuzzyCompare(minLightness, maxLightness) + ? baseLightness + : lightnessDistribution(randomEngine); - int saturationDifference = 0; - if (range.saturation != 0u) { - std::uniform_int_distribution dist(0, range.saturation); - saturationDifference = dist(randomEngine) - range.saturation / 2; - } + double red, green, blue; + hsluv2rgb(hue, saturation, lightness, &red, &green, &blue); - int valueDifference = 0; - if (range.value != 0u) { - std::uniform_int_distribution dist(0, range.value); - valueDifference = dist(randomEngine) - range.value / 2; - } - - QColor &color = entry; - - int newHue = qAbs((color.hue() + hueDifference) % MAX_HUE); - int newValue = qMin(qAbs(color.value() + valueDifference), 255); - int newSaturation = qMin(qAbs(color.saturation() + saturationDifference), 255); - - color.setHsv(newHue, newSaturation, newValue); - - return entry; + return QColor(qRound(red * 255), qRound(green * 255), qRound(blue * 255)); } void ColorScheme::getColorTable(ColorEntry *table, uint randomSeed) const @@ -297,35 +336,38 @@ void ColorScheme::getColorTable(ColorEntry *table, uint randomSeed) const } } -bool ColorScheme::randomizedBackgroundColor() const +bool ColorScheme::isColorRandomizationEnabled() const { - return _randomTable == nullptr ? false : !_randomTable[BGCOLOR_INDEX].isNull(); + return (_colorRandomization && _randomTable != nullptr); } -void ColorScheme::setRandomizedBackgroundColor(bool randomize) +void ColorScheme::setColorRandomization(bool randomize) { - // the hue of the background color is allowed to be randomly - // adjusted as much as possible. - // - // the value and saturation are left alone to maintain read-ability - // except for dark background schemes which allow a change in the - // colour value (one less than the dark background threshold) + _colorRandomization = randomize; if (randomize) { - quint8 maxValue = 0; - - if (hasDarkBackground()) { - maxValue = 126; + bool hasAnyRandomizationEntries = false; + if (_randomTable != nullptr) { + for (int i = 0; !hasAnyRandomizationEntries && i < TABLE_COLORS; i++) { + hasAnyRandomizationEntries = !_randomTable[i].isNull(); + } + } + // Set default randomization settings + if (!hasAnyRandomizationEntries) { + static const int ColorIndexesForRandomization[] = { + ColorFgIndex, ColorBgIndex, + ColorFgIntenseIndex, ColorBgIntenseIndex, + ColorFgFaintIndex, ColorBgFaintIndex, + }; + for (int index: ColorIndexesForRandomization) { + setRandomizationRange(index, MaxHue, MaxSaturation, 0.0); + } } - - setRandomizationRange(BGCOLOR_INDEX, MAX_HUE, 255, maxValue); - } else if (_randomTable != nullptr) { - setRandomizationRange(BGCOLOR_INDEX, 0, 0, 0); } } -void ColorScheme::setRandomizationRange(int index, quint16 hue, quint8 saturation, quint8 value) +void ColorScheme::setRandomizationRange(int index, double hue, double saturation, double lightness) { - Q_ASSERT(hue <= MAX_HUE); + Q_ASSERT(hue <= MaxHue); Q_ASSERT(index >= 0 && index < TABLE_COLORS); if (_randomTable == nullptr) { @@ -333,8 +375,8 @@ void ColorScheme::setRandomizationRange(int index, quint16 hue, quint8 saturatio } _randomTable[index].hue = hue; - _randomTable[index].value = value; _randomTable[index].saturation = saturation; + _randomTable[index].lightness = lightness; } const ColorEntry *ColorScheme::colorTable() const @@ -357,9 +399,12 @@ QColor ColorScheme::backgroundColor() const bool ColorScheme::hasDarkBackground() const { - // value can range from 0 - 255, with larger values indicating higher brightness. - // so 127 is in the middle, anything less is deemed 'dark' - return backgroundColor().value() < 127; + double h, s, l; + const double r = backgroundColor().redF(); + const double g = backgroundColor().greenF(); + const double b = backgroundColor().blueF(); + rgb2hsluv(r, g, b, &h, &s, &l); + return l < 0.5; } void ColorScheme::setOpacity(qreal opacity) @@ -396,6 +441,7 @@ void ColorScheme::read(const KConfig &config) setOpacity(configGroup.readEntry("Opacity", 1.0)); _blur = configGroup.readEntry("Blur", false); setWallpaper(configGroup.readEntry("Wallpaper", QString())); + _colorRandomization = configGroup.readEntry(EnableColorRandomizationKey, false); for (int i = 0; i < TABLE_COLORS; i++) { readColorEntry(config, i); @@ -416,17 +462,25 @@ void ColorScheme::readColorEntry(const KConfig &config, int index) entry = configGroup.readEntry("Color", QColor()); setColorTableEntry(index, entry); - quint16 hue = static_cast(configGroup.readEntry("MaxRandomHue", 0)); - const quint8 value = static_cast(configGroup.readEntry("MaxRandomValue", 0)); - const quint8 saturation = static_cast(configGroup.readEntry("MaxRandomSaturation", 0)); + const auto readAndCheckConfigEntry = [&](const char *key, double min, double max) -> double { + const double value = configGroup.readEntry(key, min); + if (min > value || value > max) { + qCDebug(KonsoleDebug) << QStringLiteral( + "Color scheme \"%1\": color index 2 has an invalid value: %3 = %4. " + "Allowed value range: %5 - %6. Using %7.") + .arg(name()).arg(index).arg(QLatin1String(key)).arg(value, 0, 'g', 1) + .arg(min, 0, 'g', 1).arg(max, 0, 'g', 1).arg(min, 0, 'g', 1); + return min; + } + return value; + }; - if (hue > MAX_HUE) { - qCDebug(KonsoleDebug)<<"ColorScheme"<path()); + configGroup.writeEntry(EnableColorRandomizationKey, _colorRandomization); for (int i = 0; i < TABLE_COLORS; i++) { writeColorEntry(config, i); @@ -453,25 +508,36 @@ void ColorScheme::writeColorEntry(KConfig &config, int index) const configGroup.writeEntry("Color", entry); // Remove unused keys - if (configGroup.hasKey("Transparent")) { - configGroup.deleteEntry("Transparent"); - } - if (configGroup.hasKey("Transparency")) { - configGroup.deleteEntry("Transparency"); - } - if (configGroup.hasKey("Bold")) { - configGroup.deleteEntry("Bold"); + static const char *obsoleteKeys[] = { + "Transparent", + "Transparency", + "Bold", + // Uncomment when people stop using Konsole from 2019: + // "MaxRandomHue", + // "MaxRandomValue", + // "MaxRandomSaturation" + }; + for (const auto key: obsoleteKeys) { + if (configGroup.hasKey(key)) { + configGroup.deleteEntry(key); + } } RandomizationRange random = _randomTable != nullptr ? _randomTable[index] : RandomizationRange(); - // record randomization if this color has randomization or - // if one of the keys already exists - if (!random.isNull() || configGroup.hasKey("MaxRandomHue")) { - configGroup.writeEntry("MaxRandomHue", static_cast(random.hue)); - configGroup.writeEntry("MaxRandomValue", static_cast(random.value)); - configGroup.writeEntry("MaxRandomSaturation", static_cast(random.saturation)); - } + const auto checkAndMaybeSaveValue = [&](const char *key, double value) { + const bool valueIsNull = qFuzzyCompare(value, 0.0); + const bool keyExists = configGroup.hasKey(key); + const bool keyExistsAndHasDifferentValue = !qFuzzyCompare(configGroup.readEntry(key, value), + value); + if ((!valueIsNull && !keyExists) || keyExistsAndHasDifferentValue) { + configGroup.writeEntry(key, value); + } + }; + + checkAndMaybeSaveValue(RandomHueRangeKey, random.hue); + checkAndMaybeSaveValue(RandomSaturationRangeKey, random.saturation); + checkAndMaybeSaveValue(RandomLightnessRangeKey, random.lightness); } void ColorScheme::setWallpaper(const QString &path) diff --git a/src/ColorScheme.h b/src/ColorScheme.h index 43ad15340..1a65a22a0 100644 --- a/src/ColorScheme.h +++ b/src/ColorScheme.h @@ -132,8 +132,8 @@ public: /** * Returns true if this color scheme has a dark background. - * The background color is said to be dark if it has a value of less than 127 - * in the HSV color space. + * The background color is said to be dark if it has a lightness + * of less than 50% in the HSLuv color space. */ bool hasDarkBackground() const; @@ -169,15 +169,15 @@ public: ColorSchemeWallpaper::Ptr wallpaper() const; /** - * Enables randomization of the background color. This will cause - * the palette returned by getColorTable() and colorEntry() to - * be adjusted depending on the value of the random seed argument - * to them. + * Enables colors randomization. This will cause the palette + * returned by getColorTable() and colorEntry() to be adjusted + * depending on the parameters of color randomization and the + * random seed parameter passed to them. */ - void setRandomizedBackgroundColor(bool randomize); + void setColorRandomization(bool randomize); - /** Returns true if the background color is randomized. */ - bool randomizedBackgroundColor() const; + /** Returns true if color randomization is enabled. */ + bool isColorRandomizationEnabled() const; static const ColorEntry defaultTable[]; // table of default color entries @@ -189,20 +189,20 @@ private: class RandomizationRange { public: - RandomizationRange() : hue(0), - saturation(0), - value(0) + RandomizationRange() : hue(0.0), + saturation(0.0), + lightness(0.0) { } bool isNull() const { - return hue == 0 && saturation == 0 && value == 0; + return qFuzzyIsNull(hue) && qFuzzyIsNull(saturation) && qFuzzyIsNull(lightness); } - quint16 hue; - quint8 saturation; - quint8 value; + double hue; + double saturation; + double lightness; }; // returns the active color table. if none has been set specifically, @@ -218,7 +218,7 @@ private: // sets the amount of randomization allowed for a particular color // in the palette. creates the randomization table if // it does not already exist - void setRandomizationRange(int index, quint16 hue, quint8 saturation, quint8 value); + void setRandomizationRange(int index, double hue, double saturation, double lightness); QString _description; QString _name; @@ -236,9 +236,9 @@ private: // enables blur behind the terminal window bool _blur; - ColorSchemeWallpaper::Ptr _wallpaper; + bool _colorRandomization; - static const quint16 MAX_HUE = 340; + ColorSchemeWallpaper::Ptr _wallpaper; static const char * const colorNames[TABLE_COLORS]; static const char * const translatedColorNames[TABLE_COLORS]; diff --git a/src/ColorSchemeEditor.cpp b/src/ColorSchemeEditor.cpp index 809238cc2..f84ae61f8 100644 --- a/src/ColorSchemeEditor.cpp +++ b/src/ColorSchemeEditor.cpp @@ -258,7 +258,7 @@ void ColorSchemeEditor::setBlur(bool blur) void ColorSchemeEditor::setRandomizedBackgroundColor(bool randomized) { - _colors->setRandomizedBackgroundColor(randomized); + _colors->setColorRandomization(randomized); } void ColorSchemeEditor::setup(const ColorScheme *scheme, bool isNewScheme) @@ -291,7 +291,7 @@ void ColorSchemeEditor::setup(const ColorScheme *scheme, bool isNewScheme) _ui->blurCheckBox->setChecked(scheme->blur()); // randomized background color checkbox - _ui->randomizedBackgroundCheck->setChecked(scheme->randomizedBackgroundColor()); + _ui->randomizedBackgroundCheck->setChecked(scheme->isColorRandomizationEnabled()); // wallpaper stuff _ui->wallpaperPath->setText(scheme->wallpaper()->path()); diff --git a/src/ColorSchemeEditor.ui b/src/ColorSchemeEditor.ui index a4dc0c0d5..6ed5e5562 100644 --- a/src/ColorSchemeEditor.ui +++ b/src/ColorSchemeEditor.ui @@ -45,8 +45,12 @@ + + Hue and saturation values of default foreground and background colors are randomized by default. Some color schemes might use different randomization settings. +To see any effect, set colors with saturation value greater than 0. + - Vary the background color for each tab + Randomly adjust colors for each session diff --git a/src/ViewManager.cpp b/src/ViewManager.cpp index 3efc99b66..b68822142 100644 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -774,9 +774,10 @@ void ViewManager::viewDestroyed(QWidget *view) TerminalDisplay *ViewManager::createTerminalDisplay(Session *session) { auto display = new TerminalDisplay(nullptr); - display->setRandomSeed(session->sessionId() * 31); + display->setRandomSeed(session->sessionId() | (qApp->applicationPid() << 10)); connect(display, &TerminalDisplay::requestToggleExpansion, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal); + return display; } diff --git a/src/hsluv.c b/src/hsluv.c new file mode 100644 index 000000000..7c09b38db --- /dev/null +++ b/src/hsluv.c @@ -0,0 +1,453 @@ +/* + * HSLuv-C: Human-friendly HSL + * + * + * + * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation) + * Copyright (c) 2015 Roger Tallada (Obj-C implementation) + * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * + * 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. + */ + +#include "hsluv.h" + +#include +#include + + +typedef struct Triplet_tag Triplet; +struct Triplet_tag { + double a; + double b; + double c; +}; + +/* for RGB */ +static const Triplet m[3] = { + { 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 }, + { -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 }, + { 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 } +}; + +/* for XYZ */ +static const Triplet m_inv[3] = { + { 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 }, + { 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 }, + { 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 } +}; + +static const double ref_u = 0.19783000664283680764; +static const double ref_v = 0.46831999493879100370; + +static const double kappa = 903.29629629629629629630; +static const double epsilon = 0.00885645167903563082; + + +typedef struct Bounds_tag Bounds; +struct Bounds_tag { + double a; + double b; +}; + + +static void +get_bounds(double l, Bounds bounds[6]) +{ + double tl = l + 16.0; + double sub1 = (tl * tl * tl) / 1560896.0; + double sub2 = (sub1 > epsilon ? sub1 : (l / kappa)); + int channel; + int t; + + for(channel = 0; channel < 3; channel++) { + double m1 = m[channel].a; + double m2 = m[channel].b; + double m3 = m[channel].c; + + for (t = 0; t < 2; t++) { + double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2; + double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l; + double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t; + + bounds[channel * 2 + t].a = top1 / bottom; + bounds[channel * 2 + t].b = top2 / bottom; + } + } +} + +static double +intersect_line_line(const Bounds* line1, const Bounds* line2) +{ + return (line1->b - line2->b) / (line2->a - line1->a); +} + +static double +dist_from_pole_squared(double x, double y) +{ + return x * x + y * y; +} + +static double +ray_length_until_intersect(double theta, const Bounds* line) +{ + return line->b / (sin(theta) - line->a * cos(theta)); +} + +static double +max_safe_chroma_for_l(double l) +{ + double min_len_squared = DBL_MAX; + Bounds bounds[6]; + int i; + + get_bounds(l, bounds); + for(i = 0; i < 6; i++) { + double m1 = bounds[i].a; + double b1 = bounds[i].b; + /* x where line intersects with perpendicular running though (0, 0) */ + Bounds line2 = { -1.0 / m1, 0.0 }; + double x = intersect_line_line(&bounds[i], &line2); + double distance = dist_from_pole_squared(x, b1 + x * m1); + + if(distance < min_len_squared) + min_len_squared = distance; + } + + return sqrt(min_len_squared); +} + +static double +max_chroma_for_lh(double l, double h) +{ + double min_len = DBL_MAX; + double hrad = h * 0.01745329251994329577; /* (2 * pi / 360) */ + Bounds bounds[6]; + int i; + + get_bounds(l, bounds); + for(i = 0; i < 6; i++) { + double len = ray_length_until_intersect(hrad, &bounds[i]); + + if(len >= 0 && len < min_len) + min_len = len; + } + return min_len; +} + +static double +dot_product(const Triplet* t1, const Triplet* t2) +{ + return (t1->a * t2->a + t1->b * t2->b + t1->c * t2->c); +} + +/* Used for rgb conversions */ +static double +from_linear(double c) +{ + if(c <= 0.0031308) + return 12.92 * c; + else + return 1.055 * pow(c, 1.0 / 2.4) - 0.055; +} + +static double +to_linear(double c) +{ + if (c > 0.04045) + return pow((c + 0.055) / 1.055, 2.4); + else + return c / 12.92; +} + +static void +xyz2rgb(Triplet* in_out) +{ + double r = from_linear(dot_product(&m[0], in_out)); + double g = from_linear(dot_product(&m[1], in_out)); + double b = from_linear(dot_product(&m[2], in_out)); + in_out->a = r; + in_out->b = g; + in_out->c = b; +} + +static void +rgb2xyz(Triplet* in_out) +{ + Triplet rgbl = { to_linear(in_out->a), to_linear(in_out->b), to_linear(in_out->c) }; + double x = dot_product(&m_inv[0], &rgbl); + double y = dot_product(&m_inv[1], &rgbl); + double z = dot_product(&m_inv[2], &rgbl); + in_out->a = x; + in_out->b = y; + in_out->c = z; +} + +/* http://en.wikipedia.org/wiki/CIELUV + * In these formulas, Yn refers to the reference white point. We are using + * illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is + * simplified accordingly. + */ +static double +y2l(double y) +{ + if(y <= epsilon) + return y * kappa; + else + return 116.0 * cbrt(y) - 16.0; +} + +static double +l2y(double l) +{ + if(l <= 8.0) { + return l / kappa; + } else { + double x = (l + 16.0) / 116.0; + return (x * x * x); + } +} + +static void +xyz2luv(Triplet* in_out) +{ + double var_u = (4.0 * in_out->a) / (in_out->a + (15.0 * in_out->b) + (3.0 * in_out->c)); + double var_v = (9.0 * in_out->b) / (in_out->a + (15.0 * in_out->b) + (3.0 * in_out->c)); + double l = y2l(in_out->b); + double u = 13.0 * l * (var_u - ref_u); + double v = 13.0 * l * (var_v - ref_v); + + in_out->a = l; + if(l < 0.00000001) { + in_out->b = 0.0; + in_out->c = 0.0; + } else { + in_out->b = u; + in_out->c = v; + } +} + +static void +luv2xyz(Triplet* in_out) +{ + if(in_out->a <= 0.00000001) { + /* Black will create a divide-by-zero error. */ + in_out->a = 0.0; + in_out->b = 0.0; + in_out->c = 0.0; + return; + } + + double var_u = in_out->b / (13.0 * in_out->a) + ref_u; + double var_v = in_out->c / (13.0 * in_out->a) + ref_v; + double y = l2y(in_out->a); + double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v); + double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v); + in_out->a = x; + in_out->b = y; + in_out->c = z; +} + +static void +luv2lch(Triplet* in_out) +{ + double l = in_out->a; + double u = in_out->b; + double v = in_out->c; + double h; + double c = sqrt(u * u + v * v); + + /* Grays: disambiguate hue */ + if(c < 0.00000001) { + h = 0; + } else { + h = atan2(v, u) * 57.29577951308232087680; /* (180 / pi) */ + if(h < 0.0) + h += 360.0; + } + + in_out->a = l; + in_out->b = c; + in_out->c = h; +} + +static void +lch2luv(Triplet* in_out) +{ + double hrad = in_out->c * 0.01745329251994329577; /* (pi / 180.0) */ + double u = cos(hrad) * in_out->b; + double v = sin(hrad) * in_out->b; + + in_out->b = u; + in_out->c = v; +} + +static void +hsluv2lch(Triplet* in_out) +{ + double h = in_out->a; + double s = in_out->b; + double l = in_out->c; + double c; + + /* White and black: disambiguate chroma */ + if(l > 99.9999999 || l < 0.00000001) + c = 0.0; + else + c = max_chroma_for_lh(l, h) / 100.0 * s; + + /* Grays: disambiguate hue */ + if (s < 0.00000001) + h = 0.0; + + in_out->a = l; + in_out->b = c; + in_out->c = h; +} + +static void +lch2hsluv(Triplet* in_out) +{ + double l = in_out->a; + double c = in_out->b; + double h = in_out->c; + double s; + + /* White and black: disambiguate saturation */ + if(l > 99.9999999 || l < 0.00000001) + s = 0.0; + else + s = c / max_chroma_for_lh(l, h) * 100.0; + + /* Grays: disambiguate hue */ + if (c < 0.00000001) + h = 0.0; + + in_out->a = h; + in_out->b = s; + in_out->c = l; +} + +static void +hpluv2lch(Triplet* in_out) +{ + double h = in_out->a; + double s = in_out->b; + double l = in_out->c; + double c; + + /* White and black: disambiguate chroma */ + if(l > 99.9999999 || l < 0.00000001) + c = 0.0; + else + c = max_safe_chroma_for_l(l) / 100.0 * s; + + /* Grays: disambiguate hue */ + if (s < 0.00000001) + h = 0.0; + + in_out->a = l; + in_out->b = c; + in_out->c = h; +} + +static void +lch2hpluv(Triplet* in_out) +{ + double l = in_out->a; + double c = in_out->b; + double h = in_out->c; + double s; + + /* White and black: disambiguate saturation */ + if (l > 99.9999999 || l < 0.00000001) + s = 0.0; + else + s = c / max_safe_chroma_for_l(l) * 100.0; + + /* Grays: disambiguate hue */ + if (c < 0.00000001) + h = 0.0; + + in_out->a = h; + in_out->b = s; + in_out->c = l; +} + + + +void +hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb) +{ + Triplet tmp = { h, s, l }; + + hsluv2lch(&tmp); + lch2luv(&tmp); + luv2xyz(&tmp); + xyz2rgb(&tmp); + + *pr = tmp.a; + *pg = tmp.b; + *pb = tmp.c; +} + +void +hpluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb) +{ + Triplet tmp = { h, s, l }; + + hpluv2lch(&tmp); + lch2luv(&tmp); + luv2xyz(&tmp); + xyz2rgb(&tmp); + + *pr = tmp.a; + *pg = tmp.b; + *pb = tmp.c; +} + +void +rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl) +{ + Triplet tmp = { r, g, b }; + + rgb2xyz(&tmp); + xyz2luv(&tmp); + luv2lch(&tmp); + lch2hsluv(&tmp); + + *ph = tmp.a; + *ps = tmp.b; + *pl = tmp.c; +} + +void +rgb2hpluv(double r, double g, double b, double* ph, double* ps, double* pl) +{ + Triplet tmp = { r, g, b }; + + rgb2xyz(&tmp); + xyz2luv(&tmp); + luv2lch(&tmp); + lch2hpluv(&tmp); + + *ph = tmp.a; + *ps = tmp.b; + *pl = tmp.c; +} diff --git a/src/hsluv.h b/src/hsluv.h new file mode 100644 index 000000000..ee715cd37 --- /dev/null +++ b/src/hsluv.h @@ -0,0 +1,90 @@ +/* + * HSLuv-C: Human-friendly HSL + * + * + * + * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation) + * Copyright (c) 2015 Roger Tallada (Obj-C implementation) + * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * + * 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. + */ + +#ifndef HSLUV_H +#define HSLUV_H + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * Convert HSLuv to RGB. + * + * @param h Hue. Between 0.0 and 360.0. + * @param s Saturation. Between 0.0 and 100.0. + * @param l Lightness. Between 0.0 and 100.0. + * @param[out] pr Red component. Between 0.0 and 1.0. + * @param[out] pr Green component. Between 0.0 and 1.0. + * @param[out] pr Blue component. Between 0.0 and 1.0. + */ +void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb); + +/** + * Convert RGB to HSLuv. + * + * @param r Red component. Between 0.0 and 1.0. + * @param g Green component. Between 0.0 and 1.0. + * @param b Blue component. Between 0.0 and 1.0. + * @param[out] ph Hue. Between 0.0 and 360.0. + * @param[out] ps Saturation. Between 0.0 and 100.0. + * @param[out] pl Lightness. Between 0.0 and 100.0. + */ +void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl); + +/** + * Convert HPLuv to RGB. + * + * @param h Hue. Between 0.0 and 360.0. + * @param s Saturation. Between 0.0 and 100.0. + * @param l Lightness. Between 0.0 and 100.0. + * @param[out] pr Red component. Between 0.0 and 1.0. + * @param[out] pg Green component. Between 0.0 and 1.0. + * @param[out] pb Blue component. Between 0.0 and 1.0. + */ +void hpluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb); + +/** + * Convert RGB to HPLuv. + * + * @param r Red component. Between 0.0 and 1.0. + * @param g Green component. Between 0.0 and 1.0. + * @param b Blue component. Between 0.0 and 1.0. + * @param[out] ph Hue. Between 0.0 and 360.0. + * @param[out] ps Saturation. Between 0.0 and 100.0. + * @param[out] pl Lightness. Between 0.0 and 100.0. + */ +void rgb2hpluv(double r, double g, double b, double* ph, double* ps, double* pl); + + +#ifdef __cplusplus +} +#endif + +#endif /* HSLUV_H */