File indexing completed on 2024-04-28 15:27:40
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 scaleAlpha = [](const QColor &color, double factor) { 0063 return QColor::fromRgb(color.red(), color.green(), color.blue(), color.alpha() * factor); 0064 }; 0065 auto linearlyInterpolateDouble = [](double one, double two, double factor) { 0066 return one + (two - one) * factor; 0067 }; 0068 0069 if (one == Qt::transparent) { 0070 return scaleAlpha(two, balance); 0071 } 0072 if (two == Qt::transparent) { 0073 return scaleAlpha(one, 1 - balance); 0074 } 0075 // QColor returns -1 when hue is undefined, which happens whenever 0076 // saturation is 0. When this happens, interpolation can go wrong so handle 0077 // it by first trying to use the other color's hue and if that is also -1, 0078 // just skip the hue interpolation by using 0 for both. 0079 auto sourceHue = std::max(float(one.hueF() > 0.0 ? one.hueF() : two.hueF()), 0.0f); 0080 auto targetHue = std::max(float(two.hueF() > 0.0 ? two.hueF() : one.hueF()), 0.0f); 0081 0082 auto hue = std::fmod(linearlyInterpolateDouble(sourceHue, targetHue, balance), 1.0); 0083 auto saturation = std::clamp(linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), 0.0, 1.0); 0084 auto value = std::clamp(linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), 0.0, 1.0); 0085 auto alpha = std::clamp(linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), 0.0, 1.0); 0086 0087 return QColor::fromHsvF(hue, saturation, value, alpha); 0088 } 0089 0090 // Some private things for the adjust, change, and scale properties 0091 struct ParsedAdjustments { 0092 double red = 0.0; 0093 double green = 0.0; 0094 double blue = 0.0; 0095 0096 double hue = 0.0; 0097 double saturation = 0.0; 0098 double value = 0.0; 0099 0100 double alpha = 0.0; 0101 }; 0102 0103 ParsedAdjustments parseAdjustments(const QJSValue &value) 0104 { 0105 ParsedAdjustments parsed; 0106 0107 auto checkProperty = [](const QJSValue &value, const QString &property) { 0108 if (value.hasProperty(property)) { 0109 auto val = value.property(property); 0110 if (val.isNumber()) { 0111 return QVariant::fromValue(val.toNumber()); 0112 } 0113 } 0114 return QVariant(); 0115 }; 0116 0117 std::vector<std::pair<QString, double &>> items{{QStringLiteral("red"), parsed.red}, 0118 {QStringLiteral("green"), parsed.green}, 0119 {QStringLiteral("blue"), parsed.blue}, 0120 // 0121 {QStringLiteral("hue"), parsed.hue}, 0122 {QStringLiteral("saturation"), parsed.saturation}, 0123 {QStringLiteral("value"), parsed.value}, 0124 {QStringLiteral("lightness"), parsed.value}, 0125 // 0126 {QStringLiteral("alpha"), parsed.alpha}}; 0127 0128 for (const auto &item : items) { 0129 auto val = checkProperty(value, item.first); 0130 if (val.isValid()) { 0131 item.second = val.toDouble(); 0132 } 0133 } 0134 0135 if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) { 0136 qCCritical(KirigamiLog) << "It is an error to have both RGB and HSL values in an adjustment."; 0137 } 0138 0139 return parsed; 0140 } 0141 0142 QColor ColorUtils::adjustColor(const QColor &color, const QJSValue &adjustments) 0143 { 0144 auto adjusts = parseAdjustments(adjustments); 0145 0146 if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) { 0147 qCCritical(KirigamiLog) << "Hue is out of bounds"; 0148 } 0149 if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) { 0150 qCCritical(KirigamiLog) << "Red is out of bounds"; 0151 } 0152 if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) { 0153 qCCritical(KirigamiLog) << "Green is out of bounds"; 0154 } 0155 if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) { 0156 qCCritical(KirigamiLog) << "Green is out of bounds"; 0157 } 0158 if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) { 0159 qCCritical(KirigamiLog) << "Saturation is out of bounds"; 0160 } 0161 if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) { 0162 qCCritical(KirigamiLog) << "Value is out of bounds"; 0163 } 0164 if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) { 0165 qCCritical(KirigamiLog) << "Alpha is out of bounds"; 0166 } 0167 0168 auto copy = color; 0169 0170 if (adjusts.alpha) { 0171 copy.setAlpha(adjusts.alpha); 0172 } 0173 0174 if (adjusts.red || adjusts.green || adjusts.blue) { 0175 copy.setRed(copy.red() + adjusts.red); 0176 copy.setGreen(copy.green() + adjusts.green); 0177 copy.setBlue(copy.blue() + adjusts.blue); 0178 } else if (adjusts.hue || adjusts.saturation || adjusts.value) { 0179 copy.setHsl(std::fmod(copy.hue() + adjusts.hue, 360.0), // 0180 copy.saturation() + adjusts.saturation, // 0181 copy.value() + adjusts.value, 0182 copy.alpha()); 0183 } 0184 0185 return copy; 0186 } 0187 0188 QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments) 0189 { 0190 auto adjusts = parseAdjustments(adjustments); 0191 auto copy = color; 0192 0193 if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) { 0194 qCCritical(KirigamiLog) << "Red is out of bounds"; 0195 } 0196 if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) { 0197 qCCritical(KirigamiLog) << "Green is out of bounds"; 0198 } 0199 if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) { 0200 qCCritical(KirigamiLog) << "Blue is out of bounds"; 0201 } 0202 if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) { 0203 qCCritical(KirigamiLog) << "Saturation is out of bounds"; 0204 } 0205 if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) { 0206 qCCritical(KirigamiLog) << "Value is out of bounds"; 0207 } 0208 if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) { 0209 qCCritical(KirigamiLog) << "Alpha is out of bounds"; 0210 } 0211 0212 if (adjusts.hue != 0) { 0213 qCCritical(KirigamiLog) << "Hue cannot be scaled"; 0214 } 0215 0216 auto shiftToAverage = [](double current, double factor) { 0217 auto scale = qBound(-100.0, factor, 100.0) / 100; 0218 return current + (scale > 0 ? 255 - current : current) * scale; 0219 }; 0220 0221 if (adjusts.red || adjusts.green || adjusts.blue) { 0222 copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0)); 0223 copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0)); 0224 copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0)); 0225 } else { 0226 copy.setHsl(copy.hue(), 0227 qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0), 0228 qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0), 0229 qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0)); 0230 } 0231 0232 return copy; 0233 } 0234 0235 QColor ColorUtils::tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha) 0236 { 0237 qreal tintAlpha = tintColor.alphaF() * alpha; 0238 qreal inverseAlpha = 1.0 - tintAlpha; 0239 0240 if (qFuzzyCompare(tintAlpha, 1.0)) { 0241 return tintColor; 0242 } else if (qFuzzyIsNull(tintAlpha)) { 0243 return targetColor; 0244 } 0245 0246 return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha, 0247 tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha, 0248 tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha, 0249 tintAlpha + inverseAlpha * targetColor.alphaF()); 0250 } 0251 0252 ColorUtils::XYZColor ColorUtils::colorToXYZ(const QColor &color) 0253 { 0254 // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ 0255 qreal r = color.redF(); 0256 qreal g = color.greenF(); 0257 qreal b = color.blueF(); 0258 // Apply gamma correction (i.e. conversion to linear-space) 0259 auto correct = [](qreal &v) { 0260 if (v > 0.04045) { 0261 v = std::pow((v + 0.055) / 1.055, 2.4); 0262 } else { 0263 v = v / 12.92; 0264 } 0265 }; 0266 0267 correct(r); 0268 correct(g); 0269 correct(b); 0270 0271 // Observer. = 2°, Illuminant = D65 0272 const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805; 0273 const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722; 0274 const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505; 0275 0276 return XYZColor{x, y, z}; 0277 } 0278 0279 ColorUtils::LabColor ColorUtils::colorToLab(const QColor &color) 0280 { 0281 // First: convert to XYZ 0282 const auto xyz = colorToXYZ(color); 0283 0284 // Second: convert from XYZ to L*a*b 0285 qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65 0286 qreal y = xyz.y / 1.0; 0287 qreal z = xyz.z / 1.08883; 0288 0289 auto pivot = [](qreal &v) { 0290 if (v > 0.008856) { 0291 v = std::pow(v, 1.0 / 3.0); 0292 } else { 0293 v = (7.787 * v) + (16.0 / 116.0); 0294 } 0295 }; 0296 0297 pivot(x); 0298 pivot(y); 0299 pivot(z); 0300 0301 LabColor labColor; 0302 labColor.l = std::max(0.0, (116 * y) - 16); 0303 labColor.a = 500 * (x - y); 0304 labColor.b = 200 * (y - z); 0305 0306 return labColor; 0307 } 0308 0309 qreal ColorUtils::chroma(const QColor &color) 0310 { 0311 LabColor labColor = colorToLab(color); 0312 0313 // Chroma is hypotenuse of a and b 0314 return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2)); 0315 } 0316 0317 qreal ColorUtils::luminance(const QColor &color) 0318 { 0319 const auto &xyz = colorToXYZ(color); 0320 // Luminance is equal to Y 0321 return xyz.y; 0322 } 0323 0324 #include "moc_colorutils.cpp"