MauiKit Controls

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 <QIcon>
10#include <QtMath>
11#include <cmath>
12#include <map>
13
14ColorUtils::ColorUtils(QObject *parent)
15 : QObject(parent)
16{
17}
18
20{
21 auto luma = [](const QColor &color) {
22 return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255;
23 };
24
25 return luma(color) > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark;
26}
27
28qreal ColorUtils::grayForColor(const QColor &color)
29{
30 return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255;
31}
32
33QColor ColorUtils::alphaBlend(const QColor &foreground, const QColor &background)
34{
35 const auto foregroundAlpha = foreground.alpha();
36 const auto inverseForegroundAlpha = 0xff - foregroundAlpha;
37 const auto backgroundAlpha = background.alpha();
38
39 if (foregroundAlpha == 0x00) {
40 return background;
41 }
42
43 if (backgroundAlpha == 0xff) {
44 return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()),
45 (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()),
46 (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()),
47 0xff);
48 } else {
49 const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha) / 255;
50 const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha;
51 Q_ASSERT(finalAlpha != 0x00);
52 return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()),
53 (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()),
54 (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()),
55 finalAlpha);
56 }
57}
58
59QColor ColorUtils::linearInterpolation(const QColor &one, const QColor &two, double balance)
60{
61 auto scaleAlpha = [](const QColor &color, double factor) {
62 return QColor::fromRgb(color.red(), color.green(), color.blue(), color.alpha() * factor);
63 };
64 auto linearlyInterpolateDouble = [](double one, double two, double factor) {
65 return one + (two - one) * factor;
66 };
67
68 if (one == Qt::transparent) {
69 return scaleAlpha(two, balance);
70 }
71 if (two == Qt::transparent) {
72 return scaleAlpha(one, 1 - balance);
73 }
74
75 return QColor::fromHsv(std::fmod(linearlyInterpolateDouble(one.hue(), two.hue(), balance), 360.0),
76 qBound(0.0, linearlyInterpolateDouble(one.saturation(), two.saturation(), balance), 255.0),
77 qBound(0.0, linearlyInterpolateDouble(one.value(), two.value(), balance), 255.0),
78 qBound(0.0, linearlyInterpolateDouble(one.alpha(), two.alpha(), balance), 255.0));
79
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(property)) {
101 auto val = value.property(property);
102 if (val.isNumber()) {
103 return QVariant::fromValue(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 {QStringLiteral("lightness"), parsed.value},
117 //
118 {QStringLiteral("alpha"), parsed.alpha}};
119
120 for (const auto &item : items) {
121 auto val = checkProperty(value, item.first);
122 if (val.isValid()) {
123 item.second = val.toDouble();
124 }
125 }
126
127 if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) {
128 qCritical() << "It is an error to have both RGB and HSL values in an adjustment.";
129 }
130
131 return parsed;
132}
133
134QColor ColorUtils::adjustColor(const QColor &color, const QJSValue &adjustments)
135{
136 auto adjusts = parseAdjustments(adjustments);
137
138 if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) {
139 qCritical() << "Hue is out of bounds";
140 }
141 if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) {
142 qCritical() << "Red is out of bounds";
143 }
144 if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) {
145 qCritical() << "Green is out of bounds";
146 }
147 if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) {
148 qCritical() << "Green is out of bounds";
149 }
150 if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) {
151 qCritical() << "Saturation is out of bounds";
152 }
153 if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) {
154 qCritical() << "Value is out of bounds";
155 }
156 if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) {
157 qCritical() << "Alpha is out of bounds";
158 }
159
160 auto copy = color;
161
162 if (adjusts.alpha) {
163 copy.setAlpha(adjusts.alpha);
164 }
165
166 if (adjusts.red || adjusts.green || adjusts.blue) {
167 copy.setRed(copy.red() + adjusts.red);
168 copy.setGreen(copy.green() + adjusts.green);
169 copy.setBlue(copy.blue() + adjusts.blue);
170 } else if (adjusts.hue || adjusts.saturation || adjusts.value) {
171 copy.setHsl(std::fmod(copy.hue() + adjusts.hue, 360.0), //
172 copy.saturation() + adjusts.saturation, //
173 copy.value() + adjusts.value,
174 copy.alpha());
175 }
176
177 return copy;
178}
179
180QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments)
181{
182 auto adjusts = parseAdjustments(adjustments);
183 auto copy = color;
184
185 if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) {
186 qCritical() << "Red is out of bounds";
187 }
188 if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) {
189 qCritical() << "Green is out of bounds";
190 }
191 if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) {
192 qCritical() << "Blue is out of bounds";
193 }
194 if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) {
195 qCritical() << "Saturation is out of bounds";
196 }
197 if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) {
198 qCritical() << "Value is out of bounds";
199 }
200 if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) {
201 qCritical() << "Alpha is out of bounds";
202 }
203
204 if (adjusts.hue != 0) {
205 qCritical() << "Hue cannot be scaled";
206 }
207
208 auto shiftToAverage = [](double current, double factor) {
209 auto scale = qBound(-100.0, factor, 100.0) / 100;
210 return current + (scale > 0 ? 255 - current : current) * scale;
211 };
212
213 if (adjusts.red || adjusts.green || adjusts.blue) {
214 copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0));
215 copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0));
216 copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0));
217 } else {
218 copy.setHsl(copy.hue(),
219 qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0),
220 qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0),
221 qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0));
222 }
223
224 return copy;
225}
226
227QColor ColorUtils::tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha)
228{
229 qreal tintAlpha = tintColor.alphaF() * alpha;
230 qreal inverseAlpha = 1.0 - tintAlpha;
231
232 if (qFuzzyCompare(tintAlpha, 1.0)) {
233 return tintColor;
234 } else if (qFuzzyIsNull(tintAlpha)) {
235 return targetColor;
236 }
237
238 return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha,
239 tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha,
240 tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha,
241 tintAlpha + inverseAlpha * targetColor.alphaF());
242}
243
244ColorUtils::XYZColor ColorUtils::colorToXYZ(const QColor &color)
245{
246 // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ
247 qreal r = color.redF();
248 qreal g = color.greenF();
249 qreal b = color.blueF();
250 // Apply gamma correction (i.e. conversion to linear-space)
251 auto correct = [](qreal &v) {
252 if (v > 0.04045) {
253 v = std::pow((v + 0.055) / 1.055, 2.4);
254 } else {
255 v = v / 12.92;
256 }
257 };
258
259 correct(r);
260 correct(g);
261 correct(b);
262
263 // Observer. = 2°, Illuminant = D65
264 const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805;
265 const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722;
266 const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505;
267
268 return XYZColor{x, y, z};
269}
270
271ColorUtils::LabColor ColorUtils::colorToLab(const QColor &color)
272{
273 // First: convert to XYZ
274 const auto xyz = colorToXYZ(color);
275
276 // Second: convert from XYZ to L*a*b
277 qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65
278 qreal y = xyz.y / 1.0;
279 qreal z = xyz.z / 1.08883;
280
281 auto pivot = [](qreal &v) {
282 if (v > 0.008856) {
283 v = std::pow(v, 1.0 / 3.0);
284 } else {
285 v = (7.787 * v) + (16.0 / 116.0);
286 }
287 };
288
289 pivot(x);
290 pivot(y);
291 pivot(z);
292
293 LabColor labColor;
294 labColor.l = std::max(0.0, (116 * y) - 16);
295 labColor.a = 500 * (x - y);
296 labColor.b = 200 * (y - z);
297
298 return labColor;
299}
300
301qreal ColorUtils::chroma(const QColor &color)
302{
303 LabColor labColor = colorToLab(color);
304
305 // Chroma is hypotenuse of a and b
306 return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2));
307}
308
309qreal ColorUtils::luminance(const QColor &color)
310{
311 const auto &xyz = colorToXYZ(color);
312 // Luminance is equal to Y
313 return xyz.y;
314}
315
316#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 &)
QAction * copy(const QObject *recvr, const char *slot, QObject *parent)
int alpha() const const
float alphaF() const const
int blue() const const
float blueF() const const
QColor fromHsv(int h, int s, int v, int a)
QColor fromRgb(QRgb rgb)
QColor fromRgbF(float r, float g, float b, float a)
int green() const const
float greenF() const const
int hue() const const
int red() const const
float redF() const const
int saturation() const const
int value() const const
bool hasProperty(const QString &name) const const
QJSValue property(const QString &name) const const
transparent
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 19 2024 12:00:22 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.