Kirigami2

colorutils.cpp
1/*
2 * SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "colorutils.h"
8
9#include "loggingcategory.h"
10#include <QIcon>
11#include <QtMath>
12#include <cmath>
13#include <map>
14
15ColorUtils::ColorUtils(QObject *parent)
16 : QObject(parent)
17{
18}
19
21{
22 auto luma = [](const QColor &color) {
23 return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255;
24 };
25
26 return luma(color) > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark;
27}
28
29qreal ColorUtils::grayForColor(const QColor &color)
30{
31 return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255;
32}
33
34QColor ColorUtils::alphaBlend(const QColor &foreground, const QColor &background)
35{
36 const auto foregroundAlpha = foreground.alpha();
37 const auto inverseForegroundAlpha = 0xff - foregroundAlpha;
38 const auto backgroundAlpha = background.alpha();
39
40 if (foregroundAlpha == 0x00) {
41 return background;
42 }
43
44 if (backgroundAlpha == 0xff) {
45 return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()),
46 (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()),
47 (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()),
48 0xff);
49 } else {
50 const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha) / 255;
51 const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha;
52 Q_ASSERT(finalAlpha != 0x00);
53 return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()),
54 (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()),
55 (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()),
56 finalAlpha);
57 }
58}
59
60QColor ColorUtils::linearInterpolation(const QColor &one, const QColor &two, double balance)
61{
62 auto linearlyInterpolateDouble = [](double one, double two, double factor) {
63 return one + (two - one) * factor;
64 };
65
66 // QColor returns -1 when hue is undefined, which happens whenever
67 // saturation is 0. When this happens, interpolation can go wrong so handle
68 // it by first trying to use the other color's hue and if that is also -1,
69 // just skip the hue interpolation by using 0 for both.
70 auto sourceHue = std::max(one.hueF() > 0.0 ? one.hueF() : two.hueF(), 0.0f);
71 auto targetHue = std::max(two.hueF() > 0.0 ? two.hueF() : one.hueF(), 0.0f);
72
73 auto hue = std::fmod(linearlyInterpolateDouble(sourceHue, targetHue, balance), 1.0);
74 auto saturation = std::clamp(linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), 0.0, 1.0);
75 auto value = std::clamp(linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), 0.0, 1.0);
76 auto alpha = std::clamp(linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), 0.0, 1.0);
77
78 return QColor::fromHsvF(hue, saturation, value, alpha);
79}
80
81// Some private things for the adjust, change, and scale properties
82struct ParsedAdjustments {
83 double red = 0.0;
84 double green = 0.0;
85 double blue = 0.0;
86
87 double hue = 0.0;
88 double saturation = 0.0;
89 double value = 0.0;
90
91 double alpha = 0.0;
92};
93
94ParsedAdjustments parseAdjustments(const QJSValue &value)
95{
96 ParsedAdjustments parsed;
97
98 auto checkProperty = [](const QJSValue &value, const QString &property) {
99 if (value.hasProperty(property)) {
100 auto val = value.property(property);
101 if (val.isNumber()) {
102 return QVariant::fromValue(val.toNumber());
103 }
104 }
105 return QVariant();
106 };
107
108 std::vector<std::pair<QString, double &>> items{{QStringLiteral("red"), parsed.red},
109 {QStringLiteral("green"), parsed.green},
110 {QStringLiteral("blue"), parsed.blue},
111 //
112 {QStringLiteral("hue"), parsed.hue},
113 {QStringLiteral("saturation"), parsed.saturation},
114 {QStringLiteral("value"), parsed.value},
115 //
116 {QStringLiteral("alpha"), parsed.alpha}};
117
118 for (const auto &item : items) {
119 auto val = checkProperty(value, item.first);
120 if (val.isValid()) {
121 item.second = val.toDouble();
122 }
123 }
124
125 if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) {
126 qCCritical(KirigamiLog) << "It is an error to have both RGB and HSV values in an adjustment.";
127 }
128
129 return parsed;
130}
131
132QColor ColorUtils::adjustColor(const QColor &color, const QJSValue &adjustments)
133{
134 auto adjusts = parseAdjustments(adjustments);
135
136 if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) {
137 qCCritical(KirigamiLog) << "Hue is out of bounds";
138 }
139 if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) {
140 qCCritical(KirigamiLog) << "Red is out of bounds";
141 }
142 if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) {
143 qCCritical(KirigamiLog) << "Green is out of bounds";
144 }
145 if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) {
146 qCCritical(KirigamiLog) << "Green is out of bounds";
147 }
148 if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) {
149 qCCritical(KirigamiLog) << "Saturation is out of bounds";
150 }
151 if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) {
152 qCCritical(KirigamiLog) << "Value is out of bounds";
153 }
154 if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) {
155 qCCritical(KirigamiLog) << "Alpha is out of bounds";
156 }
157
158 auto copy = color;
159
160 if (adjusts.alpha) {
161 copy.setAlpha(qBound(0.0, copy.alpha() + adjusts.alpha, 255.0));
162 }
163
164 if (adjusts.red || adjusts.green || adjusts.blue) {
165 copy.setRed(qBound(0.0, copy.red() + adjusts.red, 255.0));
166 copy.setGreen(qBound(0.0, copy.green() + adjusts.green, 255.0));
167 copy.setBlue(qBound(0.0, copy.blue() + adjusts.blue, 255.0));
168 } else if (adjusts.hue || adjusts.saturation || adjusts.value) {
169 copy.setHsv(std::fmod(copy.hue() + adjusts.hue, 360.0),
170 qBound(0.0, copy.saturation() + adjusts.saturation, 255.0),
171 qBound(0.0, copy.value() + adjusts.value, 255.0),
172 copy.alpha());
173 }
174
175 return copy;
176}
177
178QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments)
179{
180 auto adjusts = parseAdjustments(adjustments);
181 auto copy = color;
182
183 if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) {
184 qCCritical(KirigamiLog) << "Red is out of bounds";
185 }
186 if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) {
187 qCCritical(KirigamiLog) << "Green is out of bounds";
188 }
189 if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) {
190 qCCritical(KirigamiLog) << "Blue is out of bounds";
191 }
192 if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) {
193 qCCritical(KirigamiLog) << "Saturation is out of bounds";
194 }
195 if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) {
196 qCCritical(KirigamiLog) << "Value is out of bounds";
197 }
198 if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) {
199 qCCritical(KirigamiLog) << "Alpha is out of bounds";
200 }
201
202 if (adjusts.hue != 0) {
203 qCCritical(KirigamiLog) << "Hue cannot be scaled";
204 }
205
206 auto shiftToAverage = [](double current, double factor) {
207 auto scale = qBound(-100.0, factor, 100.0) / 100;
208 return current + (scale > 0 ? 255 - current : current) * scale;
209 };
210
211 if (adjusts.alpha) {
212 copy.setAlpha(qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0));
213 }
214
215 if (adjusts.red || adjusts.green || adjusts.blue) {
216 copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0));
217 copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0));
218 copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0));
219 } else {
220 copy.setHsv(copy.hue(),
221 qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0),
222 qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0),
223 copy.alpha());
224 }
225
226 return copy;
227}
228
229QColor ColorUtils::tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha)
230{
231 qreal tintAlpha = tintColor.alphaF() * alpha;
232 qreal inverseAlpha = 1.0 - tintAlpha;
233
234 if (qFuzzyCompare(tintAlpha, 1.0)) {
235 return tintColor;
236 } else if (qFuzzyIsNull(tintAlpha)) {
237 return targetColor;
238 }
239
240 return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha,
241 tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha,
242 tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha,
243 tintAlpha + inverseAlpha * targetColor.alphaF());
244}
245
246ColorUtils::XYZColor ColorUtils::colorToXYZ(const QColor &color)
247{
248 // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ
249 qreal r = color.redF();
250 qreal g = color.greenF();
251 qreal b = color.blueF();
252 // Apply gamma correction (i.e. conversion to linear-space)
253 auto correct = [](qreal &v) {
254 if (v > 0.04045) {
255 v = std::pow((v + 0.055) / 1.055, 2.4);
256 } else {
257 v = v / 12.92;
258 }
259 };
260
261 correct(r);
262 correct(g);
263 correct(b);
264
265 // Observer. = 2°, Illuminant = D65
266 const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805;
267 const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722;
268 const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505;
269
270 return XYZColor{x, y, z};
271}
272
273ColorUtils::LabColor ColorUtils::colorToLab(const QColor &color)
274{
275 // First: convert to XYZ
276 const auto xyz = colorToXYZ(color);
277
278 // Second: convert from XYZ to L*a*b
279 qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65
280 qreal y = xyz.y / 1.0;
281 qreal z = xyz.z / 1.08883;
282
283 auto pivot = [](qreal &v) {
284 if (v > 0.008856) {
285 v = std::pow(v, 1.0 / 3.0);
286 } else {
287 v = (7.787 * v) + (16.0 / 116.0);
288 }
289 };
290
291 pivot(x);
292 pivot(y);
293 pivot(z);
294
295 LabColor labColor;
296 labColor.l = std::max(0.0, (116 * y) - 16);
297 labColor.a = 500 * (x - y);
298 labColor.b = 200 * (y - z);
299
300 return labColor;
301}
302
303qreal ColorUtils::chroma(const QColor &color)
304{
305 LabColor labColor = colorToLab(color);
306
307 // Chroma is hypotenuse of a and b
308 return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2));
309}
310
311qreal ColorUtils::luminance(const QColor &color)
312{
313 const auto &xyz = colorToXYZ(color);
314 // Luminance is equal to Y
315 return xyz.y;
316}
317
318#include "moc_colorutils.cpp"
Q_INVOKABLE qreal grayForColor(const QColor &color)
Q_INVOKABLE QColor adjustColor(const QColor &color, const QJSValue &adjustments)
Q_INVOKABLE QColor alphaBlend(const QColor &foreground, const QColor &background)
Q_INVOKABLE QColor scaleColor(const QColor &color, const QJSValue &adjustments)
static Q_INVOKABLE qreal chroma(const QColor &color)
Q_INVOKABLE QColor tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha)
Q_INVOKABLE QColor linearInterpolation(const QColor &one, const QColor &two, double balance)
Q_INVOKABLE ColorUtils::Brightness brightnessForColor(const QColor &color)
KGUIADDONS_EXPORT qreal luma(const QColor &)
KGUIADDONS_EXPORT qreal hue(const QColor &)
const QList< QKeySequence > & copy()
int alpha() const const
float alphaF() const const
int blue() const const
float blueF() const const
QColor fromHsvF(float h, float s, float v, float a)
QColor fromRgb(QRgb rgb)
QColor fromRgbF(float r, float g, float b, float a)
int green() const const
float greenF() const const
float hueF() const const
int red() const const
float redF() const const
float saturationF() const const
float valueF() const const
bool hasProperty(const QString &name) const const
QJSValue property(const QString &name) const const
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:18:46 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.