File indexing completed on 2024-04-28 07:44:46
0001 /* 0002 * SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "colorutils.h" 0008 0009 #include "loggingcategory.h" 0010 #include <QIcon> 0011 #include <QtMath> 0012 #include <cmath> 0013 #include <map> 0014 0015 ColorUtils::ColorUtils(QObject *parent) 0016 : QObject(parent) 0017 { 0018 } 0019 0020 ColorUtils::Brightness ColorUtils::brightnessForColor(const QColor &color) 0021 { 0022 auto luma = [](const QColor &color) { 0023 return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255; 0024 }; 0025 0026 return luma(color) > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark; 0027 } 0028 0029 qreal ColorUtils::grayForColor(const QColor &color) 0030 { 0031 return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255; 0032 } 0033 0034 QColor ColorUtils::alphaBlend(const QColor &foreground, const QColor &background) 0035 { 0036 const auto foregroundAlpha = foreground.alpha(); 0037 const auto inverseForegroundAlpha = 0xff - foregroundAlpha; 0038 const auto backgroundAlpha = background.alpha(); 0039 0040 if (foregroundAlpha == 0x00) { 0041 return background; 0042 } 0043 0044 if (backgroundAlpha == 0xff) { 0045 return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()), 0046 (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()), 0047 (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()), 0048 0xff); 0049 } else { 0050 const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha) / 255; 0051 const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha; 0052 Q_ASSERT(finalAlpha != 0x00); 0053 return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()), 0054 (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()), 0055 (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()), 0056 finalAlpha); 0057 } 0058 } 0059 0060 QColor ColorUtils::linearInterpolation(const QColor &one, const QColor &two, double balance) 0061 { 0062 auto linearlyInterpolateDouble = [](double one, double two, double factor) { 0063 return one + (two - one) * factor; 0064 }; 0065 0066 // QColor returns -1 when hue is undefined, which happens whenever 0067 // saturation is 0. When this happens, interpolation can go wrong so handle 0068 // it by first trying to use the other color's hue and if that is also -1, 0069 // just skip the hue interpolation by using 0 for both. 0070 auto sourceHue = std::max(one.hueF() > 0.0 ? one.hueF() : two.hueF(), 0.0f); 0071 auto targetHue = std::max(two.hueF() > 0.0 ? two.hueF() : one.hueF(), 0.0f); 0072 0073 auto hue = std::fmod(linearlyInterpolateDouble(sourceHue, targetHue, balance), 1.0); 0074 auto saturation = std::clamp(linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), 0.0, 1.0); 0075 auto value = std::clamp(linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), 0.0, 1.0); 0076 auto alpha = std::clamp(linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), 0.0, 1.0); 0077 0078 return QColor::fromHsvF(hue, saturation, value, alpha); 0079 } 0080 0081 // Some private things for the adjust, change, and scale properties 0082 struct ParsedAdjustments { 0083 double red = 0.0; 0084 double green = 0.0; 0085 double blue = 0.0; 0086 0087 double hue = 0.0; 0088 double saturation = 0.0; 0089 double value = 0.0; 0090 0091 double alpha = 0.0; 0092 }; 0093 0094 ParsedAdjustments parseAdjustments(const QJSValue &value) 0095 { 0096 ParsedAdjustments parsed; 0097 0098 auto checkProperty = [](const QJSValue &value, const QString &property) { 0099 if (value.hasProperty(property)) { 0100 auto val = value.property(property); 0101 if (val.isNumber()) { 0102 return QVariant::fromValue(val.toNumber()); 0103 } 0104 } 0105 return QVariant(); 0106 }; 0107 0108 std::vector<std::pair<QString, double &>> items{{QStringLiteral("red"), parsed.red}, 0109 {QStringLiteral("green"), parsed.green}, 0110 {QStringLiteral("blue"), parsed.blue}, 0111 // 0112 {QStringLiteral("hue"), parsed.hue}, 0113 {QStringLiteral("saturation"), parsed.saturation}, 0114 {QStringLiteral("value"), parsed.value}, 0115 // 0116 {QStringLiteral("alpha"), parsed.alpha}}; 0117 0118 for (const auto &item : items) { 0119 auto val = checkProperty(value, item.first); 0120 if (val.isValid()) { 0121 item.second = val.toDouble(); 0122 } 0123 } 0124 0125 if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) { 0126 qCCritical(KirigamiLog) << "It is an error to have both RGB and HSV values in an adjustment."; 0127 } 0128 0129 return parsed; 0130 } 0131 0132 QColor ColorUtils::adjustColor(const QColor &color, const QJSValue &adjustments) 0133 { 0134 auto adjusts = parseAdjustments(adjustments); 0135 0136 if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) { 0137 qCCritical(KirigamiLog) << "Hue is out of bounds"; 0138 } 0139 if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) { 0140 qCCritical(KirigamiLog) << "Red is out of bounds"; 0141 } 0142 if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) { 0143 qCCritical(KirigamiLog) << "Green is out of bounds"; 0144 } 0145 if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) { 0146 qCCritical(KirigamiLog) << "Green is out of bounds"; 0147 } 0148 if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) { 0149 qCCritical(KirigamiLog) << "Saturation is out of bounds"; 0150 } 0151 if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) { 0152 qCCritical(KirigamiLog) << "Value is out of bounds"; 0153 } 0154 if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) { 0155 qCCritical(KirigamiLog) << "Alpha is out of bounds"; 0156 } 0157 0158 auto copy = color; 0159 0160 if (adjusts.alpha) { 0161 copy.setAlpha(qBound(0.0, copy.alpha() + adjusts.alpha, 255.0)); 0162 } 0163 0164 if (adjusts.red || adjusts.green || adjusts.blue) { 0165 copy.setRed(qBound(0.0, copy.red() + adjusts.red, 255.0)); 0166 copy.setGreen(qBound(0.0, copy.green() + adjusts.green, 255.0)); 0167 copy.setBlue(qBound(0.0, copy.blue() + adjusts.blue, 255.0)); 0168 } else if (adjusts.hue || adjusts.saturation || adjusts.value) { 0169 copy.setHsv(std::fmod(copy.hue() + adjusts.hue, 360.0), 0170 qBound(0.0, copy.saturation() + adjusts.saturation, 255.0), 0171 qBound(0.0, copy.value() + adjusts.value, 255.0), 0172 copy.alpha()); 0173 } 0174 0175 return copy; 0176 } 0177 0178 QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments) 0179 { 0180 auto adjusts = parseAdjustments(adjustments); 0181 auto copy = color; 0182 0183 if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) { 0184 qCCritical(KirigamiLog) << "Red is out of bounds"; 0185 } 0186 if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) { 0187 qCCritical(KirigamiLog) << "Green is out of bounds"; 0188 } 0189 if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) { 0190 qCCritical(KirigamiLog) << "Blue is out of bounds"; 0191 } 0192 if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) { 0193 qCCritical(KirigamiLog) << "Saturation is out of bounds"; 0194 } 0195 if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) { 0196 qCCritical(KirigamiLog) << "Value is out of bounds"; 0197 } 0198 if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) { 0199 qCCritical(KirigamiLog) << "Alpha is out of bounds"; 0200 } 0201 0202 if (adjusts.hue != 0) { 0203 qCCritical(KirigamiLog) << "Hue cannot be scaled"; 0204 } 0205 0206 auto shiftToAverage = [](double current, double factor) { 0207 auto scale = qBound(-100.0, factor, 100.0) / 100; 0208 return current + (scale > 0 ? 255 - current : current) * scale; 0209 }; 0210 0211 if (adjusts.alpha) { 0212 copy.setAlpha(qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0)); 0213 } 0214 0215 if (adjusts.red || adjusts.green || adjusts.blue) { 0216 copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0)); 0217 copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0)); 0218 copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0)); 0219 } else { 0220 copy.setHsv(copy.hue(), 0221 qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0), 0222 qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0), 0223 copy.alpha()); 0224 } 0225 0226 return copy; 0227 } 0228 0229 QColor ColorUtils::tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha) 0230 { 0231 qreal tintAlpha = tintColor.alphaF() * alpha; 0232 qreal inverseAlpha = 1.0 - tintAlpha; 0233 0234 if (qFuzzyCompare(tintAlpha, 1.0)) { 0235 return tintColor; 0236 } else if (qFuzzyIsNull(tintAlpha)) { 0237 return targetColor; 0238 } 0239 0240 return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha, 0241 tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha, 0242 tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha, 0243 tintAlpha + inverseAlpha * targetColor.alphaF()); 0244 } 0245 0246 ColorUtils::XYZColor ColorUtils::colorToXYZ(const QColor &color) 0247 { 0248 // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ 0249 qreal r = color.redF(); 0250 qreal g = color.greenF(); 0251 qreal b = color.blueF(); 0252 // Apply gamma correction (i.e. conversion to linear-space) 0253 auto correct = [](qreal &v) { 0254 if (v > 0.04045) { 0255 v = std::pow((v + 0.055) / 1.055, 2.4); 0256 } else { 0257 v = v / 12.92; 0258 } 0259 }; 0260 0261 correct(r); 0262 correct(g); 0263 correct(b); 0264 0265 // Observer. = 2°, Illuminant = D65 0266 const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805; 0267 const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722; 0268 const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505; 0269 0270 return XYZColor{x, y, z}; 0271 } 0272 0273 ColorUtils::LabColor ColorUtils::colorToLab(const QColor &color) 0274 { 0275 // First: convert to XYZ 0276 const auto xyz = colorToXYZ(color); 0277 0278 // Second: convert from XYZ to L*a*b 0279 qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65 0280 qreal y = xyz.y / 1.0; 0281 qreal z = xyz.z / 1.08883; 0282 0283 auto pivot = [](qreal &v) { 0284 if (v > 0.008856) { 0285 v = std::pow(v, 1.0 / 3.0); 0286 } else { 0287 v = (7.787 * v) + (16.0 / 116.0); 0288 } 0289 }; 0290 0291 pivot(x); 0292 pivot(y); 0293 pivot(z); 0294 0295 LabColor labColor; 0296 labColor.l = std::max(0.0, (116 * y) - 16); 0297 labColor.a = 500 * (x - y); 0298 labColor.b = 200 * (y - z); 0299 0300 return labColor; 0301 } 0302 0303 qreal ColorUtils::chroma(const QColor &color) 0304 { 0305 LabColor labColor = colorToLab(color); 0306 0307 // Chroma is hypotenuse of a and b 0308 return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2)); 0309 } 0310 0311 qreal ColorUtils::luminance(const QColor &color) 0312 { 0313 const auto &xyz = colorToXYZ(color); 0314 // Luminance is equal to Y 0315 return xyz.y; 0316 } 0317 0318 #include "moc_colorutils.cpp"