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 <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
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((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()),
47 (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()),
48 (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()),
49 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((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()),
55 (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()),
56 (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()),
57 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(one.hueF() > 0.0 ? one.hueF() : two.hueF(), 0.0f);
72 auto targetHue = std::max(two.hueF() > 0.0 ? two.hueF() : one.hueF(), 0.0f);
73
74 auto hue = std::fmod(linearlyInterpolateDouble(sourceHue, targetHue, balance), 1.0);
75 auto saturation = std::clamp(linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), 0.0, 1.0);
76 auto value = std::clamp(linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), 0.0, 1.0);
77 auto alpha = std::clamp(linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), 0.0, 1.0);
78
79 return QColor::fromHsvF(hue, saturation, value, 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(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 //
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(adjustments);
136
137 if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) {
138 qCCritical(KirigamiPlatform) << "Hue is out of bounds";
139 }
140 if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) {
141 qCCritical(KirigamiPlatform) << "Red is out of bounds";
142 }
143 if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) {
144 qCCritical(KirigamiPlatform) << "Green is out of bounds";
145 }
146 if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) {
147 qCCritical(KirigamiPlatform) << "Green is out of bounds";
148 }
149 if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) {
150 qCCritical(KirigamiPlatform) << "Saturation is out of bounds";
151 }
152 if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) {
153 qCCritical(KirigamiPlatform) << "Value is out of bounds";
154 }
155 if (qBound(-255.0, adjusts.alpha, 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(0.0, copy.alpha() + adjusts.alpha, 255.0));
163 }
164
165 if (adjusts.red || adjusts.green || adjusts.blue) {
166 copy.setRed(qBound(0.0, copy.red() + adjusts.red, 255.0));
167 copy.setGreen(qBound(0.0, copy.green() + adjusts.green, 255.0));
168 copy.setBlue(qBound(0.0, copy.blue() + adjusts.blue, 255.0));
169 } else if (adjusts.hue || adjusts.saturation || adjusts.value) {
170 copy.setHsv(std::fmod(copy.hue() + adjusts.hue, 360.0),
171 qBound(0.0, copy.saturation() + adjusts.saturation, 255.0),
172 qBound(0.0, copy.value() + adjusts.value, 255.0),
173 copy.alpha());
174 }
175
176 return copy;
177}
178
179QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments)
180{
181 auto adjusts = parseAdjustments(adjustments);
182 auto copy = color;
183
184 if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) {
185 qCCritical(KirigamiPlatform) << "Red is out of bounds";
186 }
187 if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) {
188 qCCritical(KirigamiPlatform) << "Green is out of bounds";
189 }
190 if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) {
191 qCCritical(KirigamiPlatform) << "Blue is out of bounds";
192 }
193 if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) {
194 qCCritical(KirigamiPlatform) << "Saturation is out of bounds";
195 }
196 if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) {
197 qCCritical(KirigamiPlatform) << "Value is out of bounds";
198 }
199 if (qBound(-100.0, adjusts.alpha, 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(-100.0, factor, 100.0) / 100;
209 return current + (scale > 0 ? 255 - current : current) * scale;
210 };
211
212 if (adjusts.alpha) {
213 copy.setAlpha(qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0));
214 }
215
216 if (adjusts.red || adjusts.green || adjusts.blue) {
217 copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0));
218 copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0));
219 copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0));
220 } else {
221 copy.setHsv(copy.hue(),
222 qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0),
223 qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0),
224 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(tintAlpha, 1.0)) {
236 return tintColor;
237 } else if (qFuzzyIsNull(tintAlpha)) {
238 return targetColor;
239 }
240
241 return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha,
242 tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha,
243 tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha,
244 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((v + 0.055) / 1.055, 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, y, 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(v, 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(0.0, (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(pow(labColor.a, 2) + pow(labColor.b, 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"
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 Mon Nov 18 2024 12:16:21 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.