diff --git a/data/color-schemes/BlackOnRandomLight.colorscheme b/data/color-schemes/BlackOnRandomLight.colorscheme --- 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 @@ [General] Description=Black on Random Light +ColorRandomization=true Opacity=1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,7 @@ 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 --- a/src/CharacterColor.h +++ b/src/CharacterColor.h @@ -43,6 +43,41 @@ #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.h b/src/ColorScheme.h --- a/src/ColorScheme.h +++ b/src/ColorScheme.h @@ -132,8 +132,8 @@ /** * 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 @@ 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 @@ 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 @@ // 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 @@ // 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/ColorScheme.cpp b/src/ColorScheme.cpp --- a/src/ColorScheme.cpp +++ b/src/ColorScheme.cpp @@ -21,9 +21,11 @@ // Own #include "ColorScheme.h" +#include "hsluv.h" // Qt #include +#include // KDE #include @@ -36,8 +38,17 @@ #include "konsoledebug.h" namespace { -const int FGCOLOR_INDEX = 0; -const int BGCOLOR_INDEX = 1; +static const int FGCOLOR_INDEX = 0; +static const int BGCOLOR_INDEX = 1; + +static const char RandomHueRangeKey[] = "RandomHueRange"; +static const char RandomSaturationRangeKey[] = "RandomSaturationRange"; +static const char RandomLightnessRangeKey[] = "RandomLightnessRange"; +static const char EnableColorRandomizationKey[] = "ColorRandomization"; + +static const double MaxHue = 360.0; +static const double MaxSaturation = 100.0; +static const double MaxLightness = 100.0; } using namespace Konsole; @@ -193,7 +204,7 @@ 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,44 +261,72 @@ 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 // Can't use default_random_engine, because in GCC this maps to // 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); - } - - int saturationDifference = 0; - if (range.saturation != 0u) { - std::uniform_int_distribution dist(0, range.saturation); - saturationDifference = dist(randomEngine) - range.saturation / 2; - } - - int valueDifference = 0; - if (range.value != 0u) { - std::uniform_int_distribution dist(0, range.value); - valueDifference = dist(randomEngine) - range.value / 2; + // 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); - 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); + double red, green, blue; + hsluv2rgb(hue, saturation, lightness, &red, &green, &blue); - 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,44 +336,47 @@ } } -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) { _randomTable = new RandomizationRange[TABLE_COLORS]; } _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 @@ 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 @@ 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 @@ 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 @@ 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/ColorSchemeEditor.cpp b/src/ColorSchemeEditor.cpp --- a/src/ColorSchemeEditor.cpp +++ b/src/ColorSchemeEditor.cpp @@ -258,7 +258,7 @@ void ColorSchemeEditor::setRandomizedBackgroundColor(bool randomized) { - _colors->setRandomizedBackgroundColor(randomized); + _colors->setColorRandomization(randomized); } void ColorSchemeEditor::setup(const ColorScheme *scheme, bool isNewScheme) @@ -291,7 +291,7 @@ _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 --- 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 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -774,9 +774,10 @@ 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.h b/src/hsluv.h new file mode 100644 --- /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 */ diff --git a/src/hsluv.c b/src/hsluv.c new file mode 100644 --- /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; +}