Keep perceived contrast in random background color

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
This commit is contained in:
Mariusz Glebocki
2019-08-17 16:00:45 -04:00
committed by Kurt Hindenburg
parent 74a0121422
commit dc29be39a6
10 changed files with 756 additions and 97 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -21,9 +21,11 @@
// Own
#include "ColorScheme.h"
#include "hsluv.h"
// Qt
#include <QPainter>
#include <QVariant>
// KDE
#include <KConfig>
@@ -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<int> 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<double> 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<int> 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<int> 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<quint16>(configGroup.readEntry("MaxRandomHue", 0));
const quint8 value = static_cast<quint8>(configGroup.readEntry("MaxRandomValue", 0));
const quint8 saturation = static_cast<quint8>(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"<<name()<<"has an invalid MaxRandomHue"<<hue<<"for index"<< index<<", using"<<MAX_HUE;
hue = MAX_HUE;
}
double hue = readAndCheckConfigEntry(RandomHueRangeKey, 0.0, MaxHue);
double saturation = readAndCheckConfigEntry(RandomSaturationRangeKey, 0.0, MaxSaturation);
double lightness = readAndCheckConfigEntry(RandomLightnessRangeKey, 0.0, MaxLightness);
if (hue != 0 || value != 0 || saturation != 0) {
setRandomizationRange(index, hue, saturation, value);
if (!qFuzzyIsNull(hue) || !qFuzzyIsNull(saturation) || !qFuzzyIsNull(lightness)) {
setRandomizationRange(index, hue, saturation, lightness);
}
}
@@ -438,6 +492,7 @@ void ColorScheme::write(KConfig &config) const
configGroup.writeEntry("Opacity", _opacity);
configGroup.writeEntry("Blur", _blur);
configGroup.writeEntry("Wallpaper", _wallpaper->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<int>(random.hue));
configGroup.writeEntry("MaxRandomValue", static_cast<int>(random.value));
configGroup.writeEntry("MaxRandomSaturation", static_cast<int>(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)

View File

@@ -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];

View File

@@ -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());

View File

@@ -45,8 +45,12 @@
</item>
<item>
<widget class="QCheckBox" name="randomizedBackgroundCheck">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Vary the background color for each tab</string>
<string>Randomly adjust colors for each session</string>
</property>
</widget>
</item>

View File

@@ -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;
}

453
src/hsluv.c Normal file
View File

@@ -0,0 +1,453 @@
/*
* HSLuv-C: Human-friendly HSL
* <http://github.com/hsluv/hsluv-c>
* <http://www.hsluv.org/>
*
* 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 <float.h>
#include <math.h>
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;
}

90
src/hsluv.h Normal file
View File

@@ -0,0 +1,90 @@
/*
* HSLuv-C: Human-friendly HSL
* <http://github.com/hsluv/hsluv-c>
* <http://www.hsluv.org/>
*
* 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 */