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
20ColorUtils::Brightness ColorUtils::brightnessForColor(const QColor &color)
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(r: (foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()),
46 g: (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()),
47 b: (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()),
48 a: 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(r: (foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()),
54 g: (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()),
55 b: (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()),
56 a: 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(a: one.hueF() > 0.0 ? one.hueF() : two.hueF(), b: 0.0f);
71 auto targetHue = std::max(a: two.hueF() > 0.0 ? two.hueF() : one.hueF(), b: 0.0f);
72
73 auto hue = std::fmod(x: linearlyInterpolateDouble(sourceHue, targetHue, balance), y: 1.0);
74 auto saturation = std::clamp(val: linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), lo: 0.0, hi: 1.0);
75 auto value = std::clamp(val: linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), lo: 0.0, hi: 1.0);
76 auto alpha = std::clamp(val: linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), lo: 0.0, hi: 1.0);
77
78 return QColor::fromHsvF(h: hue, s: saturation, v: value, a: 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(name: property)) {
100 auto val = value.property(name: property);
101 if (val.isNumber()) {
102 return QVariant::fromValue(value: 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(value: adjustments);
135
136 if (qBound(min: -360.0, val: adjusts.hue, max: 360.0) != adjusts.hue) {
137 qCCritical(KirigamiLog) << "Hue is out of bounds";
138 }
139 if (qBound(min: -255.0, val: adjusts.red, max: 255.0) != adjusts.red) {
140 qCCritical(KirigamiLog) << "Red is out of bounds";
141 }
142 if (qBound(min: -255.0, val: adjusts.green, max: 255.0) != adjusts.green) {
143 qCCritical(KirigamiLog) << "Green is out of bounds";
144 }
145 if (qBound(min: -255.0, val: adjusts.blue, max: 255.0) != adjusts.blue) {
146 qCCritical(KirigamiLog) << "Green is out of bounds";
147 }
148 if (qBound(min: -255.0, val: adjusts.saturation, max: 255.0) != adjusts.saturation) {
149 qCCritical(KirigamiLog) << "Saturation is out of bounds";
150 }
151 if (qBound(min: -255.0, val: adjusts.value, max: 255.0) != adjusts.value) {
152 qCCritical(KirigamiLog) << "Value is out of bounds";
153 }
154 if (qBound(min: -255.0, val: adjusts.alpha, max: 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(min: 0.0, val: copy.alpha() + adjusts.alpha, max: 255.0));
162 }
163
164 if (adjusts.red || adjusts.green || adjusts.blue) {
165 copy.setRed(qBound(min: 0.0, val: copy.red() + adjusts.red, max: 255.0));
166 copy.setGreen(qBound(min: 0.0, val: copy.green() + adjusts.green, max: 255.0));
167 copy.setBlue(qBound(min: 0.0, val: copy.blue() + adjusts.blue, max: 255.0));
168 } else if (adjusts.hue || adjusts.saturation || adjusts.value) {
169 copy.setHsv(h: std::fmod(x: copy.hue() + adjusts.hue, y: 360.0),
170 s: qBound(min: 0.0, val: copy.saturation() + adjusts.saturation, max: 255.0),
171 v: qBound(min: 0.0, val: copy.value() + adjusts.value, max: 255.0),
172 a: copy.alpha());
173 }
174
175 return copy;
176}
177
178QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments)
179{
180 auto adjusts = parseAdjustments(value: adjustments);
181 auto copy = color;
182
183 if (qBound(min: -100.0, val: adjusts.red, max: 100.00) != adjusts.red) {
184 qCCritical(KirigamiLog) << "Red is out of bounds";
185 }
186 if (qBound(min: -100.0, val: adjusts.green, max: 100.00) != adjusts.green) {
187 qCCritical(KirigamiLog) << "Green is out of bounds";
188 }
189 if (qBound(min: -100.0, val: adjusts.blue, max: 100.00) != adjusts.blue) {
190 qCCritical(KirigamiLog) << "Blue is out of bounds";
191 }
192 if (qBound(min: -100.0, val: adjusts.saturation, max: 100.00) != adjusts.saturation) {
193 qCCritical(KirigamiLog) << "Saturation is out of bounds";
194 }
195 if (qBound(min: -100.0, val: adjusts.value, max: 100.00) != adjusts.value) {
196 qCCritical(KirigamiLog) << "Value is out of bounds";
197 }
198 if (qBound(min: -100.0, val: adjusts.alpha, max: 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(min: -100.0, val: factor, max: 100.0) / 100;
208 return current + (scale > 0 ? 255 - current : current) * scale;
209 };
210
211 if (adjusts.alpha) {
212 copy.setAlpha(qBound(min: 0.0, val: shiftToAverage(copy.alpha(), adjusts.alpha), max: 255.0));
213 }
214
215 if (adjusts.red || adjusts.green || adjusts.blue) {
216 copy.setRed(qBound(min: 0.0, val: shiftToAverage(copy.red(), adjusts.red), max: 255.0));
217 copy.setGreen(qBound(min: 0.0, val: shiftToAverage(copy.green(), adjusts.green), max: 255.0));
218 copy.setBlue(qBound(min: 0.0, val: shiftToAverage(copy.blue(), adjusts.blue), max: 255.0));
219 } else {
220 copy.setHsv(h: copy.hue(),
221 s: qBound(min: 0.0, val: shiftToAverage(copy.saturation(), adjusts.saturation), max: 255.0),
222 v: qBound(min: 0.0, val: shiftToAverage(copy.value(), adjusts.value), max: 255.0),
223 a: 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(p1: tintAlpha, p2: 1.0)) {
235 return tintColor;
236 } else if (qFuzzyIsNull(d: tintAlpha)) {
237 return targetColor;
238 }
239
240 return QColor::fromRgbF(r: tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha,
241 g: tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha,
242 b: tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha,
243 a: 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(x: (v + 0.055) / 1.055, y: 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: x, .y: y, .z: 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(x: v, y: 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(a: 0.0, b: (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(x: pow(x: labColor.a, y: 2) + pow(x: labColor.b, y: 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"
319

source code of kirigami/src/colorutils.cpp