diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ include(CPack) include(FeatureSummary) -find_package(Qt5 ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Gui Svg QuickControls2) +find_package(Qt5 ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS Core Concurrent Quick Gui Svg QuickControls2) if (BUILD_TESTING) find_package(Qt5QuickTest ${REQUIRED_QT_VERSION} CONFIG QUIET) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -26,6 +26,7 @@ wheelhandler.cpp shadowedrectangle.cpp shadowedtexture.cpp + colorutils.cpp scenegraph/shadowedrectanglenode.cpp scenegraph/shadowedrectanglematerial.cpp scenegraph/shadowedborderrectanglematerial.cpp @@ -85,7 +86,7 @@ PUBLIC Qt5::Core PRIVATE - ${Kirigami_EXTRA_LIBS} Qt5::Qml Qt5::Quick Qt5::QuickControls2 + ${Kirigami_EXTRA_LIBS} Qt5::Qml Qt5::Quick Qt5::QuickControls2 Qt5::Concurrent ) if (BUILD_SHARED_LIBS) diff --git a/src/colorutils.h b/src/colorutils.h new file mode 100644 --- /dev/null +++ b/src/colorutils.h @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +/** + * Utilities for processing items to obtain colors and information useful for + * UIs that need to adjust to variable elements. + */ +class ColorUtils : public QObject +{ + Q_OBJECT +public: + /** + * Describes the contrast of an item. + */ + enum Brightness { + Dark, /**< The item is dark and requires a light foreground color to achieve readable contrast. */ + Light, /**< The item is light and requires a dark foreground color to achieve readable contrast. */ + }; + Q_ENUM(Brightness) + + explicit ColorUtils(QObject* parent = nullptr); + + /** + * Returns whether a color is bright or dark. + * + * @code{.qml} + * import QtQuick 2.0 + * import org.kde.kirigami 2.12 as Kirigami + * + * Kirigami.Heading { + * text: { + * if (Kirigami.ColorUtils.brightnessForColor("pink") == Kirigami.ColorUtils.Light) { + * return "The color is light" + * } else { + * return "The color is dark" + * } + * } + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE ColorUtils::Brightness brightnessForColor(QColor color); + + /** + * Returns the result of overlaying the foreground color on the background + * color. + * + * @param foreground The color to overlay on the background. + * + * @param background The color to overlay the foreground on. + * + * @code{.qml} + * import QtQuick 2.0 + * import org.kde.kirigami 2.12 as Kirigami + * + * Rectangle { + * color: Kirigami.ColorUtils.alphaBlend(Qt.rgba(0, 0, 0, 0.5), Qt.rgba(1, 1, 1, 1)) + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor alphaBlend(QColor foreground, QColor background); + + /** + * Returns a linearly interpolated color between color one and color two. + * + * @param one The color to linearly interpolate from. + * + * @param two The color to linearly interpolate to. + * + * @param balance The balance between the two colors. 0.0 will return the + * first color, 1.0 will return the second color. Values beyond these bounds + * are valid, and will result in extrapolation. + * + * @code{.qml} + * import QtQuick 2.0 + * import org.kde.kirigami 2.12 as Kirigami + * + * Rectangle { + * color: Kirigami.ColorUtils.linearInterpolation("black", "white", 0.5) + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor linearInterpolation(QColor one, QColor two, double balance); + + /** + * Increases or decreases the properties of `color` by fixed amounts. + * + * @param color The color to adjust. + * + * @param adjustments The adjustments to apply to the color. + * + * @note `value` and `lightness` are aliases for the same value. + * + * @code{.js} + * { + * red: null, // Range: -255 to 255 + * green: null, // Range: -255 to 255 + * blue: null, // Range: -255 to 255 + * hue: null, // Range: -360 to 360 + * saturation: null, // Range: -255 to 255 + * value: null // Range: -255 to 255 + * lightness: null, // Range: -255 to 255 + * alpha: null, // Range: -255 to 255 + * } + * @endcode + * + * @warning It is an error to adjust both RGB and HSL properties. + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor adjustColor(QColor color, QJSValue adjustments); + + /** + * Smoothly scales colors. + * + * @param color The color to adjust. + * + * @param adjustments The adjustments to apply to the color. Each value must + * be between `-100.0` and `100.0`. This indicates how far the property should + * be scaled from its original to the maximum if positive or to the minumum if + * negative. + * + * @note `value` and `lightness` are aliases for the same value. + * + * @code{.js} + * { + * red: null + * green: null + * blue: null + * saturation: null + * lightness: null + * value: null + * alpha: null + * } + * @endcode + * + * @warning It is an error to scale both RGB and HSL properties. + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor scaleColor(QColor color, QJSValue adjustments); +}; diff --git a/src/colorutils.cpp b/src/colorutils.cpp new file mode 100644 --- /dev/null +++ b/src/colorutils.cpp @@ -0,0 +1,193 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "colorutils.h" + +#include +#include +#include +#include +#include + +ColorUtils::ColorUtils(QObject *parent) : QObject(parent) {} + +ColorUtils::Brightness ColorUtils::brightnessForColor(QColor color) { + auto luma = [](QColor color) { + return (0.299*color.red() + 0.587*color.green() + 0.114*color.blue())/255; + }; + + return luma(color) > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark; +} + +QColor ColorUtils::alphaBlend(QColor foreground, QColor background) { + const auto foregroundAlpha = foreground.alpha(); + const auto inverseForegroundAlpha = 0xff - foregroundAlpha; + const auto backgroundAlpha = background.alpha(); + + if (foregroundAlpha == 0x00) return background; + + if (backgroundAlpha == 0xff) { + return QColor::fromRgb( + (foregroundAlpha*foreground.red()) + (inverseForegroundAlpha*background.red()), + (foregroundAlpha*foreground.green()) + (inverseForegroundAlpha*background.green()), + (foregroundAlpha*foreground.blue()) + (inverseForegroundAlpha*background.blue()), + 0xff + ); + } else { + const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha) / 255; + const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha; + Q_ASSERT(finalAlpha != 0x00); + return QColor::fromRgb( + (foregroundAlpha*foreground.red()) + (inverseBackgroundAlpha*background.red()), + (foregroundAlpha*foreground.green()) + (inverseBackgroundAlpha*background.green()), + (foregroundAlpha*foreground.blue()) + (inverseBackgroundAlpha*background.blue()), + finalAlpha + ); + } +} + +QColor ColorUtils::linearInterpolation(QColor one, QColor two, double balance) { + + auto scaleAlpha = [](QColor color, double factor) { + return QColor::fromRgb(color.red(), color.green(), color.blue(), color.alpha()*factor); + }; + auto linearlyInterpolateDouble = [](double one, double two, double factor) { + return one + (two - one) * factor; + }; + + if (one == Qt::transparent) return scaleAlpha(two, balance); + if (two == Qt::transparent) return scaleAlpha(one, 1 - balance); + + return QColor::fromHsv( + std::fmod(linearlyInterpolateDouble(one.hue(), two.hue(), balance), 360.0), + qBound(0.0, linearlyInterpolateDouble(one.saturation(), two.saturation(), balance), 255.0), + qBound(0.0, linearlyInterpolateDouble(one.value(), two.value(), balance), 255.0), + qBound(0.0, linearlyInterpolateDouble(one.alpha(), two.alpha(), balance), 255.0) + ); +} + +// Some private things for the adjust, change, and scale properties +struct ParsedAdjustments +{ + double red = 0.0; + double green = 0.0; + double blue = 0.0; + + double hue = 0.0; + double saturation = 0.0; + double value = 0.0; + + double alpha = 0.0; +}; + +ParsedAdjustments parseAdjustments(QJSValue value) +{ + ParsedAdjustments parsed; + + auto checkProperty = [](QJSValue value, QString property) { + if (value.hasProperty(property)) { + auto val = value.property(property); + if (val.isNumber()) { + return QVariant::fromValue(val.toNumber()); + } + } + return QVariant(); + }; + + std::map map = { + { QStringLiteral("red"), parsed.red }, + { QStringLiteral("green"), parsed.green }, + { QStringLiteral("blue"), parsed.blue }, + // + { QStringLiteral("hue"), parsed.hue }, + { QStringLiteral("saturation"), parsed.saturation }, + { QStringLiteral("value"), parsed.value }, + { QStringLiteral("lightness"), parsed.value }, + // + { QStringLiteral("alpha"), parsed.alpha } + }; + + for (std::pair item : map) { + auto val = checkProperty(value, item.first); + if (val != QVariant()) item.second = val.toDouble(); + } + + if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) { + qCritical() << "It is an error to have both RGB and HSL values in an adjustment."; + } + + return parsed; +} + +QColor ColorUtils::adjustColor(QColor color, QJSValue adjustments) +{ + auto adjusts = parseAdjustments(adjustments); + + if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) qCritical() << "Hue is out of bounds"; + + if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) qCritical() << "Red is out of bounds"; + if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) qCritical() << "Green is out of bounds"; + if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) qCritical() << "Green is out of bounds"; + if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) qCritical() << "Saturation is out of bounds"; + if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) qCritical() << "Value is out of bounds"; + if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) qCritical() << "Alpha is out of bounds"; + + auto copy = color; + + if (adjusts.alpha) { + copy.setAlpha(adjusts.alpha); + } + + if (adjusts.red || adjusts.green || adjusts.blue) { + copy.setRed(copy.red() + adjusts.red); + copy.setGreen(copy.green() + adjusts.green); + copy.setBlue(copy.blue() + adjusts.blue); + } else if (adjusts.hue || adjusts.saturation || adjusts.value) { + copy.setHsl( + std::fmod(copy.hue()+adjusts.hue, 360.0), + copy.saturation()+adjusts.saturation, + copy.value()+adjusts.value, + copy.alpha() + ); + } + + return copy; +} + +QColor ColorUtils::scaleColor(QColor color, QJSValue adjustments) +{ + auto adjusts = parseAdjustments(adjustments); + auto copy = color; + + if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) qCritical() << "Red is out of bounds"; + if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) qCritical() << "Green is out of bounds"; + if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) qCritical() << "Blue is out of bounds"; + if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) qCritical() << "Saturation is out of bounds"; + if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) qCritical() << "Value is out of bounds"; + if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) qCritical() << "Alpha is out of bounds"; + + if (adjusts.hue != 0) qCritical() << "Hue cannot be scaled"; + + auto shiftToAverage = [](double current, double factor) { + auto scale = qBound(-100.0, factor, 100.0)/100; + return current + (scale > 0 ? 255 - current : current) * scale; + }; + + if (adjusts.red || adjusts.green || adjusts.blue) { + copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0)); + copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0)); + copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0)); + } else { + copy.setHsl( + copy.hue(), + qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0), + qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0), + qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0) + ); + } + + return copy; +} \ No newline at end of file diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -19,6 +19,7 @@ #include "wheelhandler.h" #include "shadowedrectangle.h" #include "shadowedtexture.h" +#include "colorutils.h" #include #include @@ -244,6 +245,7 @@ qmlRegisterUncreatableType(uri, 2, 12, "BorderGroup", QStringLiteral("Used as grouped property")); qmlRegisterUncreatableType(uri, 2, 12, "ShadowGroup", QStringLiteral("Used as grouped property")); + qmlRegisterSingletonType(uri, 2, 12, "ColorUtils", [](QQmlEngine*, QJSEngine*) { return new ColorUtils; }); qmlRegisterUncreatableType(uri, 2, 12, "CornersGroup", QStringLiteral("Used as grouped property"));