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

source code of kirigami/src/platform/colorutils.cpp