diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ee84171 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,148 @@ +cmake_minimum_required(VERSION 3.14) + +project( + QmlMaterial + VERSION 0.1 + LANGUAGES CXX) + +option(QM_BUILD_STATIC "build static plugin" OFF) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(QM_BUILD SHARED) +if(QM_BUILD_STATIC) + set(QM_BUILD STATIC) +endif() + +find_package(Qt6 REQUIRED COMPONENTS Core Quick QuickControls2 Gui) +find_package(PkgConfig REQUIRED) + +set(LIBNAME qml_material) + +set(SOURCES + include/qml_material/util.h + include/qml_material/theme.h + include/qml_material/color.h + include/qml_material/corner.h + include/qml_material/icon.h + include/qml_material/input_block.h + include/qml_material/item_holder.h + include/qml_material/type.h + include/qml_material/round_item.h + include/qml_material/helper.h + include/qml_material/type_scale.h + include/qml_material/kirigami/wheelhandler.h + src/kirigami/wheelhandler.cpp + src/type.cpp + src/color.cpp + src/corner.cpp + src/icon.cpp + src/input_block.cpp + src/item_holder.cpp + src/image.cpp + src/util.cpp + src/state.cpp + src/theme.cpp + src/helper.cpp + src/type_scale.cpp + src/round_item.cpp) +set(QML_FILES + qml/Button.qml + qml/Control.qml + qml/ComboBox.qml + qml/Dialog.qml + qml/Divider.qml + qml/Drawer.qml + qml/DrawerItem.qml + qml/Token.qml + qml/IconLabel.qml + qml/IconButton.qml + qml/FAB.qml + qml/Card.qml + qml/CircularIndicator.qml + qml/Menu.qml + qml/MenuItem.qml + qml/Image.qml + qml/ListItem.qml + qml/ListView.qml + qml/Label.qml + qml/FontMetrics.qml + qml/Flickable.qml + qml/GridView.qml + qml/Icon.qml + qml/Text.qml + qml/Pane.qml + qml/Page.qml + qml/StackView.qml + qml/SplitView.qml + qml/Slider.qml + qml/SearchBar.qml + qml/ScrollBar.qml + qml/Switch.qml + qml/Rail.qml + qml/TextInput.qml + qml/TextField.qml + qml/TabBar.qml + qml/TabButton.qml + qml/impl/BoxShadow.qml + qml/impl/Shadow.qml + qml/impl/ListBusyFooter.qml + qml/impl/Ripple.qml + qml/impl/RectangularGlow.qml + qml/impl/CursorDelegate.qml + qml/impl/State.qml + qml/impl/SliderHandle.qml + qml/impl/RoundedElevationEffect.qml + qml/impl/ElevationEffect.qml + qml/impl/TextFieldEmbed.qml + qml/impl/RoundClip.qml) +set(RESOURCES assets/MaterialIconsRound-Regular.otf + assets/MaterialIconsOutlined-Regular.otf) + +set_source_files_properties(qml/Token.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) +include_directories(include) + +qt_add_qml_module( + ${LIBNAME} + URI + Qcm.Material + ${QM_BUILD} + RESOURCE_PREFIX + / + QML_FILES + ${QML_FILES} + SOURCES + ${SOURCES} + RESOURCES + ${RESOURCES} + NO_PLUGIN_OPTIONAL) + +set(SHADER_GLSLS "assets/shader/sdf.glsl") +set(SHADER_SOURCES + "assets/shader/round.frag" + "assets/shader/shadow.frag" + "assets/shader/rect_glow.frag" +) +qt_add_shaders(${LIBNAME} "shader" PREFIX "/Qcm/Material" FILES + ${SHADER_SOURCES}) +foreach(file ${SHADER_SOURCES}) + set(output_file ".qsb/${file}.qsb") + add_custom_command( + OUTPUT ${output_file} + DEPENDS ${SHADER_GLSLS} + APPEND) +endforeach() + +target_include_directories( + ${LIBNAME} + PUBLIC include + PRIVATE "include/${LIBNAME}" "include/${LIBNAME}/kirigami") + +add_subdirectory(third_party) + +target_link_libraries( + ${LIBNAME} + PUBLIC Qt6::Core Qt6::Quick Qt6::QuickControls2 Qt6::GuiPrivate core + PRIVATE material_color) diff --git a/assets/MaterialIconsOutlined-Regular.otf b/assets/MaterialIconsOutlined-Regular.otf new file mode 100644 index 0000000..9dad12b Binary files /dev/null and b/assets/MaterialIconsOutlined-Regular.otf differ diff --git a/assets/MaterialIconsRound-Regular.otf b/assets/MaterialIconsRound-Regular.otf new file mode 100644 index 0000000..dacf094 Binary files /dev/null and b/assets/MaterialIconsRound-Regular.otf differ diff --git a/assets/shader/rect_glow.frag b/assets/shader/rect_glow.frag new file mode 100644 index 0000000..f5dddae --- /dev/null +++ b/assets/shader/rect_glow.frag @@ -0,0 +1,28 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float relativeSizeX; + float relativeSizeY; + float spread; + vec4 color; +} ubuf; + +float linearstep(float e0, float e1, float x) +{ + return clamp((x - e0) / (e1 - e0), 0.0, 1.0); +} + +void main() +{ + float alpha = + smoothstep(0.0, ubuf.relativeSizeX, 0.5 - abs(0.5 - qt_TexCoord0.x)) * + smoothstep(0.0, ubuf.relativeSizeY, 0.5 - abs(0.5 - qt_TexCoord0.y)); + + float spreadMultiplier = linearstep(ubuf.spread, 1.0 - ubuf.spread, alpha); + fragColor = ubuf.color * ubuf.qt_Opacity * spreadMultiplier * spreadMultiplier; +} \ No newline at end of file diff --git a/assets/shader/round.frag b/assets/shader/round.frag new file mode 100644 index 0000000..a08ea14 --- /dev/null +++ b/assets/shader/round.frag @@ -0,0 +1,25 @@ +#version 440 + +#extension GL_GOOGLE_include_directive : enable + +#include "sdf.glsl" + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + // tl,tr,bl,br + vec4 radius_; + float size; + float smoothing; +}; +layout(binding = 1) uniform sampler2D source; + +void main() { + vec2 p = qt_TexCoord0 * 2.0 - vec2(1.0); + + float sdf = sdf_rounded_rectangle(p, vec2(1.0), 2.0 * radius_ / size); + + fragColor = sdf_render(sdf, vec4(0), texture(source, qt_TexCoord0), 1.0, smoothing, -1.0); +} \ No newline at end of file diff --git a/assets/shader/sdf.glsl b/assets/shader/sdf.glsl new file mode 100644 index 0000000..ad8daef --- /dev/null +++ b/assets/shader/sdf.glsl @@ -0,0 +1,42 @@ +// Signed distance function + +// according to https://iquilezles.org/articles/distfunctions2d/ + +float sdf_circle(vec2 p, float r) { return length(p) - r; } + +float sdf_rectangle(in vec2 p, in vec2 rect) { + vec2 d = abs(p) - rect; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); +} + +// tr,br,bl,tl +float sdf_rounded_rectangle(in vec2 p, in vec2 rect, in vec4 r) { + r.xy = (p.x > 0.0) ? r.xy : r.zw; + r.x = (p.y > 0.0) ? r.x : r.y; + vec2 q = abs(p) - rect + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +const float sdf_default_smoothing = 0.625; + +vec4 sdf_render(in float sdf, in vec4 sourceColor, in vec4 sdfColor, in float alpha, in float smoothing, in float offset) +{ + // bigger when zoom out + float g = smoothing * fwidth(sdf); + return mix(sourceColor, sdfColor, alpha * (1.0 - clamp((1.0 / g) * sdf - offset, 0.0, 1.0))); +} + +vec4 sdf_render(in float sdf, in vec4 sourceColor, in vec4 sdfColor, in float alpha, in float smoothing) +{ + return sdf_render(sdf, sourceColor, sdfColor, alpha, smoothing, 0); +} + +vec4 sdf_render(in float sdf, in vec4 sourceColor, in vec4 sdfColor, in float alpha) +{ + return sdf_render(sdf, sourceColor, sdfColor, alpha, sdf_default_smoothing); +} + +vec4 sdf_render(in float sdf, in vec4 sourceColor, in vec4 sdfColor) +{ + return sdf_render(sdf, sourceColor, sdfColor, 1.0); +} \ No newline at end of file diff --git a/assets/shader/shadow.frag b/assets/shader/shadow.frag new file mode 100644 index 0000000..1927899 --- /dev/null +++ b/assets/shader/shadow.frag @@ -0,0 +1,70 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec2 lower; + vec2 higher; + float sigma; + float corner; + vec4 color; +} ubuf; + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + const float pi = 3.141592653589793; + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +vec2 erf(vec2 x) { + vec2 s = sign(x), a = abs(x); + x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + x *= x; + return s - s / (x * x); +} + +// Return the blurred mask along the x dimension +float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) { + float delta = min(halfSize.y - corner - abs(y), 0.0); + float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); + vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +// Return the mask for the shadow of a box from lower to upper +float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) { + // Center everything to make the math easier + vec2 center = (lower + upper) * 0.5; + vec2 halfSize = (upper - lower) * 0.5; + point -= center; + + // The signal is only non-zero in a limited range, so don't waste samples + float low = point.y - halfSize.y; + float high = point.y + halfSize.y; + float start = clamp(-3.0 * sigma, low, high); + float end = clamp(3.0 * sigma, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.0; + float y = start + step * 0.5; + float value = 0.0; + for (int i = 0; i < 4; i++) { + value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step; + y += step; + } + + return value; +} + +void main() +{ + vec2 uv = qt_TexCoord0; + + float distShadow = roundedBoxShadow(ubuf.lower, ubuf.higher, uv, ubuf.sigma, ubuf.corner); + fragColor = ubuf.color * ubuf.qt_Opacity * distShadow; +} \ No newline at end of file diff --git a/include/qml_material/color.h b/include/qml_material/color.h new file mode 100644 index 0000000..7bf6680 --- /dev/null +++ b/include/qml_material/color.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include +#include + +#include "core/core.h" +#include "qml_material/helper.h" + +namespace qml_material +{ + +struct QColorCompare { + using is_transparent = void; + bool operator()(const QColor& a, const QColor& b) const noexcept { return a.rgba() < b.rgba(); } +}; + +class MdColorMgr : public QObject { + Q_OBJECT + QML_ELEMENT +public: + using Self = MdColorMgr; + + MdColorMgr(QObject* = nullptr); + + enum ColorSchemeEnum + { + Light, + Dark + }; + Q_ENUMS(ColorSchemeEnum) + + Q_PROPERTY(ColorSchemeEnum colorScheme READ colorScheme WRITE set_colorScheme NOTIFY + colorSchemeChanged) + Q_PROPERTY(QColor accentColor READ accentColor WRITE set_accentColor NOTIFY accentColorChanged) + Q_PROPERTY( + bool useSysColorSM READ useSysColorSM WRITE set_useSysColorSM NOTIFY useSysColorSMChanged) + Q_PROPERTY(bool useSysAccentColor READ useSysAccentColor WRITE set_useSysAccentColor NOTIFY + useSysAccentColorChanged) + +#define X(_n_) \ + Q_PROPERTY(QColor _n_ READ _n_ NOTIFY schemeChanged) \ + QColor _n_() const { return m_scheme._n_; } + + X(primary) + X(on_primary) + X(primary_container) + X(on_primary_container) + X(secondary) + X(on_secondary) + X(secondary_container) + X(on_secondary_container) + X(tertiary) + X(on_tertiary) + X(tertiary_container) + X(on_tertiary_container) + X(error) + X(on_error) + X(error_container) + X(on_error_container) + X(background) + X(on_background) + X(surface) + X(on_surface) + X(surface_variant) + X(on_surface_variant) + X(outline) + X(outline_variant) + X(shadow) + X(scrim) + X(inverse_surface) + X(inverse_on_surface) + X(inverse_primary) + X(surface_1) + X(surface_2) + X(surface_3) + X(surface_4) + X(surface_5) + X(surface_dim) + X(surface_bright) + X(surface_container) + X(surface_container_low) + X(surface_container_lowest) + X(surface_container_high) + X(surface_container_highest) +#undef X + +public: + ColorSchemeEnum colorScheme() const; + ColorSchemeEnum sysColorScheme() const; + + QColor sysAccentColor() const; + QColor accentColor() const; + bool useSysColorSM() const; + bool useSysAccentColor() const; + + Q_INVOKABLE QColor getOn(QColor) const; + +public slots: + void set_colorScheme(ColorSchemeEnum); + void set_accentColor(QColor); + void set_useSysColorSM(bool); + void set_useSysAccentColor(bool); + void gen_scheme(); + void refrehFromSystem(); + +signals: + void colorSchemeChanged(); + void schemeChanged(); + void accentColorChanged(); + void useSysColorSMChanged(); + void useSysAccentColorChanged(); + +private: + QColor m_accent_color; + ColorSchemeEnum m_color_scheme; + qcm::MdScheme m_scheme; + std::map m_on_map; + bool m_use_sys_color_scheme; + bool m_use_sys_accent_color; +}; + +} // namespace qml_material diff --git a/include/qml_material/corner.h b/include/qml_material/corner.h new file mode 100644 index 0000000..489aace --- /dev/null +++ b/include/qml_material/corner.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include + +namespace qml_material +{ +class CornersGroup { + Q_GADGET + QML_VALUE_TYPE(corner_t) + + Q_PROPERTY(qreal topLeft READ topLeft WRITE setTopLeft FINAL) + Q_PROPERTY(qreal topRight READ topRight WRITE setTopRight FINAL) + Q_PROPERTY(qreal bottomLeft READ bottomLeft WRITE setBottomLeft FINAL) + Q_PROPERTY(qreal bottomRight READ bottomRight WRITE setBottomRight FINAL) + +public: + CornersGroup(); + CornersGroup(qreal); + CornersGroup(qreal bottomRight, qreal topRight, qreal bottomLeft, qreal topLeft); + ~CornersGroup(); + + Q_INVOKABLE CornersGroup& operator=(qreal) { return *this; } + + qreal topLeft() const; + void setTopLeft(qreal newTopLeft); + + qreal topRight() const; + void setTopRight(qreal newTopRight); + + qreal bottomLeft() const; + void setBottomLeft(qreal newBottomLeft); + + qreal bottomRight() const; + void setBottomRight(qreal newBottomRight); + + Q_INVOKABLE QVector4D toVector4D() const; + +private: + float m_bottomRight; + float m_topRight; + float m_bottomLeft; + float m_topLeft; +}; + +} // namespace qml_material \ No newline at end of file diff --git a/include/qml_material/helper.h b/include/qml_material/helper.h new file mode 100644 index 0000000..aa74b7b --- /dev/null +++ b/include/qml_material/helper.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +namespace qcm +{ +struct MdScheme { + QRgb primary; + QRgb on_primary; + QRgb primary_container; + QRgb on_primary_container; + QRgb secondary; + QRgb on_secondary; + QRgb secondary_container; + QRgb on_secondary_container; + QRgb tertiary; + QRgb on_tertiary; + QRgb tertiary_container; + QRgb on_tertiary_container; + QRgb error; + QRgb on_error; + QRgb error_container; + QRgb on_error_container; + QRgb background; + QRgb on_background; + QRgb surface; + QRgb on_surface; + QRgb surface_variant; + QRgb on_surface_variant; + QRgb outline; + QRgb outline_variant; + QRgb shadow; + QRgb scrim; + QRgb inverse_surface; + QRgb inverse_on_surface; + QRgb inverse_primary; + + // surface + QRgb surface_1; + QRgb surface_2; + QRgb surface_3; + QRgb surface_4; + QRgb surface_5; + + // surface v2 + QRgb surface_dim; + QRgb surface_bright; + QRgb surface_container; + QRgb surface_container_low; + QRgb surface_container_lowest; + QRgb surface_container_high; + QRgb surface_container_highest; +}; + +MdScheme MaterialLightColorScheme(QRgb); +MdScheme MaterialDarkColorScheme(QRgb); + +QRgb MaterialBlendHctHue(const QRgb design_color, const QRgb key_color, const double mount); +} // namespace qcm diff --git a/include/qml_material/icon.h b/include/qml_material/icon.h new file mode 100644 index 0000000..e5a42c8 --- /dev/null +++ b/include/qml_material/icon.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace qml_material +{ + +class IconToken : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QVariantMap codeMap READ codeMap NOTIFY codeMapChanged) +public: + using QObject::QObject; + + const QVariantMap& codeMap() const; + +Q_SIGNALS: + void codeMapChanged(); +}; + +} // namespace qml_material + diff --git a/include/qml_material/image.h b/include/qml_material/image.h new file mode 100644 index 0000000..640cc6c --- /dev/null +++ b/include/qml_material/image.h @@ -0,0 +1,36 @@ +#include +#include + +namespace qml_material +{ + +struct CornersMaskRef { + CornersMaskRef() = default; + explicit CornersMaskRef(std::span masks) + : p { &masks[0], &masks[1], &masks[2], &masks[3] } {} + explicit CornersMaskRef(std::array masks) + : p { &masks[0], &masks[1], &masks[2], &masks[3] } {} + explicit CornersMaskRef(std::span masks) + : p { masks[0], masks[1], masks[2], masks[3] } {} + explicit CornersMaskRef(std::array masks) + : p { masks[0], masks[1], masks[2], masks[3] } {} + + [[nodiscard]] bool empty() const { return ! p[0] && ! p[1] && ! p[2] && ! p[3]; } + + std::array p {}; + + friend inline constexpr std::strong_ordering operator<=>(CornersMaskRef a, + CornersMaskRef b) noexcept { + for (auto i = 0; i != 4; ++i) { + if (a.p[i] < b.p[i]) { + return std::strong_ordering::less; + } else if (a.p[i] > b.p[i]) { + return std::strong_ordering::greater; + } + } + return std::strong_ordering::equal; + } + friend inline constexpr bool operator==(CornersMaskRef a, CornersMaskRef b) noexcept = default; +}; + +} // namespace qml_image \ No newline at end of file diff --git a/include/qml_material/input_block.h b/include/qml_material/input_block.h new file mode 100644 index 0000000..be3be54 --- /dev/null +++ b/include/qml_material/input_block.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +namespace qml_material +{ + +class InputBlock : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(bool when READ when WRITE setWhen NOTIFY whenChanged) + Q_PROPERTY(QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged) + Q_PROPERTY(Qt::MouseButtons acceptMouseButtons READ acceptMouseButtons WRITE + setAcceptMouseButtons NOTIFY acceptMouseButtonsChanged) + Q_PROPERTY(bool acceptHover READ acceptHoverEvents WRITE setAcceptHoverEvents NOTIFY + acceptHoverEventsChanged) + Q_PROPERTY(bool acceptTouch READ acceptTouchEvents WRITE setAcceptTouchEvents NOTIFY + acceptTouchEventsChanged) + +public: + InputBlock(QObject* parent = nullptr); + + QQuickItem* target() const; + void setTarget(QQuickItem*); + + bool when() const; + void setWhen(bool); + + Qt::MouseButtons acceptMouseButtons() const; + void setAcceptMouseButtons(Qt::MouseButtons buttons); + bool acceptHoverEvents() const; + void setAcceptHoverEvents(bool enabled); + bool acceptTouchEvents() const; + void setAcceptTouchEvents(bool accept); + +signals: + void whenChanged(); + void targetChanged(); + void acceptMouseButtonsChanged(); + void acceptHoverEventsChanged(); + void acceptTouchEventsChanged(); + +public slots: + void trigger(); + +private: + struct State { + bool canHover { false }; + bool canTouch { false }; + Qt::MouseButtons mouseButtons { Qt::NoButton }; + void saveState(QQuickItem*); + void restoreState(QQuickItem*); + }; + +private: + bool mWhen; + QPointer mTarget; + State mState; + State mReqState; +}; + +} // namespace qml_material \ No newline at end of file diff --git a/include/qml_material/item_holder.h b/include/qml_material/item_holder.h new file mode 100644 index 0000000..fb9361d --- /dev/null +++ b/include/qml_material/item_holder.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace qml_material +{ +class ItemHolder : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QObject* item READ item WRITE setItem NOTIFY itemChanged) + Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged) +public: + ItemHolder(QObject* parent = nullptr); + ~ItemHolder(); + + QObject* item() const; + bool visible() const; + +public Q_SLOTS: + void setItem(QObject*); + void setVisible(bool); + void refreshParent(); + +Q_SIGNALS: + void itemChanged(); + void visibleChanged(); + +private: + QPointer m_item; + bool m_visible; +}; + +} // namespace qml_material \ No newline at end of file diff --git a/include/qml_material/kirigami/wheelhandler.h b/include/qml_material/kirigami/wheelhandler.h new file mode 100644 index 0000000..eca8e6d --- /dev/null +++ b/include/qml_material/kirigami/wheelhandler.h @@ -0,0 +1,416 @@ +/* SPDX-FileCopyrightText: 2019 Marco Martin + * SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QWheelEvent; +class QQmlEngine; +class WheelHandler; + +/** + * Describes the mouse wheel event + */ +class KirigamiWheelEvent : public QObject { + Q_OBJECT + + /** + * x: real + * + * X coordinate of the mouse pointer + */ + Q_PROPERTY(qreal x READ x CONSTANT FINAL) + + /** + * y: real + * + * Y coordinate of the mouse pointer + */ + Q_PROPERTY(qreal y READ y CONSTANT FINAL) + + /** + * angleDelta: point + * + * The distance the wheel is rotated in degrees. + * The x and y coordinates indicate the horizontal and vertical wheels respectively. + * A positive value indicates it was rotated up/right, negative, bottom/left + * This value is more likely to be set in traditional mice. + */ + Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT FINAL) + + /** + * pixelDelta: point + * + * provides the delta in screen pixels available on high resolution trackpads + */ + Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT FINAL) + + /** + * buttons: int + * + * it contains an OR combination of the buttons that were pressed during the wheel, they can be: + * Qt.LeftButton, Qt.MiddleButton, Qt.RightButton + */ + Q_PROPERTY(int buttons READ buttons CONSTANT FINAL) + + /** + * modifiers: int + * + * Keyboard mobifiers that were pressed during the wheel event, such as: + * Qt.NoModifier (default, no modifiers) + * Qt.ControlModifier + * Qt.ShiftModifier + * ... + */ + Q_PROPERTY(int modifiers READ modifiers CONSTANT FINAL) + + /** + * inverted: bool + * + * Whether the delta values are inverted + * On some platformsthe returned delta are inverted, so positive values would mean bottom/left + */ + Q_PROPERTY(bool inverted READ inverted CONSTANT FINAL) + + /** + * accepted: bool + * + * If set, the event shouldn't be managed anymore, + * for instance it can be used to block the handler to manage the scroll of a view on some + * scenarios + * @code + * // This handler handles automatically the scroll of + * // flickableItem, unless Ctrl is pressed, in this case the + * // app has custom code to handle Ctrl+wheel zooming + * Kirigami.WheelHandler { + * target: flickableItem + * blockTargetWheel: true + * scrollFlickableTarget: true + * onWheel: { + * if (wheel.modifiers & Qt.ControlModifier) { + * wheel.accepted = true; + * // Handle scaling of the view + * } + * } + * } + * @endcode + * + */ + Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted FINAL) + +public: + KirigamiWheelEvent(QObject* parent = nullptr); + ~KirigamiWheelEvent() override; + + void initializeFromEvent(QWheelEvent* event); + + qreal x() const; + qreal y() const; + QPointF angleDelta() const; + QPointF pixelDelta() const; + int buttons() const; + int modifiers() const; + bool inverted() const; + bool isAccepted(); + void setAccepted(bool accepted); + +private: + qreal m_x = 0; + qreal m_y = 0; + QPointF m_angleDelta; + QPointF m_pixelDelta; + Qt::MouseButtons m_buttons = Qt::NoButton; + Qt::KeyboardModifiers m_modifiers = Qt::NoModifier; + bool m_inverted = false; + bool m_accepted = false; +}; + +class WheelFilterItem : public QQuickItem { + Q_OBJECT +public: + WheelFilterItem(QQuickItem* parent = nullptr); +}; + +/** + * @brief Handles scrolling for a Flickable and 2 attached ScrollBars. + * + * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar. + * Wheel and KeyPress events (when `keyNavigationEnabled` is true) are used to scroll the Flickable. + * When `filterMouseEvents` is true, WheelHandler blocks mouse button input from reaching the + * Flickable and sets the `interactive` property of the scrollbars to false when touch input is + * used. + * + * Wheel event handling behavior: + * + * - Pixel delta is ignored unless angle delta is not available because pixel delta scrolling is too + * slow. Qt Widgets doesn't use pixel delta either, so the default scroll speed should be consistent + * with Qt Widgets. + * - When using angle delta, scroll using the step increments defined by `verticalStepSize` and + * `horizontalStepSize`. + * - When one of the keyboard modifiers in `pageScrollModifiers` is used, scroll by pages. + * - When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the + * `verticalStepSize`, `horizontalStepSize` and page increments (if using page scrolling) will be + * multiplied by `angle delta / 120` to keep scrolling smooth. + * - If scrolling has happened in the last 400ms, use an internal QQuickItem stacked over the + * Flickable's contentItem to catch wheel events and use those wheel events to scroll, if possible. + * This prevents controls inside the Flickable's contentItem that allow scrolling to change the + * value (e.g., Sliders, SpinBoxes) from conflicting with scrolling the page. + * + * Common usage with a Flickable: + * + * @include wheelhandler/FlickableUsage.qml + * + * Common usage inside of a ScrollView template: + * + * @include wheelhandler/ScrollViewUsage.qml + * + */ +class WheelHandler : public QObject, public QQmlParserStatus { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_ELEMENT + + /** + * @brief This property holds the Qt Quick Flickable that the WheelHandler will control. + */ + Q_PROPERTY(QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged FINAL) + + /** + * @brief This property holds the vertical step size. + * + * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent + * with the default increment for QScrollArea. + * + * @sa horizontalStepSize + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(qreal verticalStepSize READ verticalStepSize WRITE setVerticalStepSize RESET + resetVerticalStepSize NOTIFY verticalStepSizeChanged FINAL) + + /** + * @brief This property holds the horizontal step size. + * + * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent + * with the default increment for QScrollArea. + * + * @sa verticalStepSize + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(qreal horizontalStepSize READ horizontalStepSize WRITE setHorizontalStepSize RESET + resetHorizontalStepSize NOTIFY horizontalStepSizeChanged FINAL) + + /** + * @brief This property holds the keyboard modifiers that will be used to start page scrolling. + * + * The default value is equivalent to `Qt.ControlModifier | Qt.ShiftModifier`. This matches + * QScrollBar, which uses QAbstractSlider behavior. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers WRITE + setPageScrollModifiers RESET resetPageScrollModifiers NOTIFY + pageScrollModifiersChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler filters mouse events like a Qt Quick + * Controls ScrollView would. + * + * Touch events are allowed to flick the view and they make the scrollbars not interactive. + * + * Mouse events are not allowed to flick the view and they make the scrollbars interactive. + * + * Hover events on the scrollbars and wheel events on anything also make the scrollbars + * interactive when this property is set to true. + * + * The default value is `false`. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents WRITE setFilterMouseEvents NOTIFY + filterMouseEventsChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler handles keyboard scrolling. + * + * - Left arrow scrolls a step to the left. + * - Right arrow scrolls a step to the right. + * - Up arrow scrolls a step upwards. + * - Down arrow scrolls a step downwards. + * - PageUp scrolls to the previous page. + * - PageDown scrolls to the next page. + * - Home scrolls to the beginning. + * - End scrolls to the end. + * - When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End. + * + * The default value is `false`. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled WRITE setKeyNavigationEnabled + NOTIFY keyNavigationEnabledChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler blocks all wheel events from reaching the + * Flickable. + * + * When this property is false, scrolling the Flickable with WheelHandler will only block an + * event from reaching the Flickable if the Flickable is actually scrolled by WheelHandler. + * + * NOTE: Wheel events created by touchpad gestures with pixel deltas will always be accepted no + * matter what. This is because they will cause the Flickable to jump back to where scrolling + * started unless the events are always accepted before they reach the Flickable. + * + * The default value is true. + */ + Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler can use wheel events to scroll the + * Flickable. + * + * The default value is true. + */ + Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY + scrollFlickableTargetChanged FINAL) + + Q_PROPERTY( + bool useAnimation READ useAnimation WRITE setUseAnimation NOTIFY useAnimationChanged FINAL) + +public: + explicit WheelHandler(QObject* parent = nullptr); + ~WheelHandler() override; + + QQuickItem* target() const; + void setTarget(QQuickItem* target); + + qreal verticalStepSize() const; + void setVerticalStepSize(qreal stepSize); + void resetVerticalStepSize(); + + qreal horizontalStepSize() const; + void setHorizontalStepSize(qreal stepSize); + void resetHorizontalStepSize(); + + Qt::KeyboardModifiers pageScrollModifiers() const; + void setPageScrollModifiers(Qt::KeyboardModifiers modifiers); + void resetPageScrollModifiers(); + + bool filterMouseEvents() const; + void setFilterMouseEvents(bool enabled); + + bool keyNavigationEnabled() const; + void setKeyNavigationEnabled(bool enabled); + + bool useAnimation() const; + void setUseAnimation(bool); + + Q_INVOKABLE bool scrollUp(qreal stepSize = -1); + Q_INVOKABLE bool scrollDown(qreal stepSize = -1); + + Q_INVOKABLE bool scrollLeft(qreal stepSize = -1); + Q_INVOKABLE bool scrollRight(qreal stepSize = -1); + +Q_SIGNALS: + void targetChanged(); + void verticalStepSizeChanged(); + void horizontalStepSizeChanged(); + void pageScrollModifiersChanged(); + void filterMouseEventsChanged(); + void keyNavigationEnabledChanged(); + void blockTargetWheelChanged(); + void scrollFlickableTargetChanged(); + void useAnimationChanged(); + + /** + * @brief This signal is emitted when a wheel event reaches the event filter, just before + * scrolling is handled. + * + * Accepting the wheel event in the `onWheel` signal handler prevents scrolling from happening. + */ + void wheel(KirigamiWheelEvent* wheel); + void wheelMoved(); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; + +private Q_SLOTS: + void refreshAttach(); + void attach(); + void detach(); + void rebindScrollBarV(); + void rebindScrollBarH(); + +private: + struct ScrollBar; + void rebindScrollBar(ScrollBar& scrollBar); + void classBegin() override; + void componentComplete() override; + + void setScrolling(bool scrolling); + bool scrollFlickable(QPointF pixelDelta, QPointF angleDelta = {}, + Qt::KeyboardModifiers modifiers = Qt::NoModifier); + + QPointer m_target; + + QMetaObject::Connection m_verticalChangedConnection; + QMetaObject::Connection m_horizontalChangedConnection; + + // Matches QScrollArea and QTextEdit + qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines(); + qreal m_verticalStepSize = m_defaultPixelStepSize; + qreal m_horizontalStepSize = m_defaultPixelStepSize; + bool m_explicitVStepSize = false; + bool m_explicitHStepSize = false; + bool m_wheelScrolling = false; + constexpr static qreal m_wheelScrollingDuration = 400; + bool m_filterMouseEvents = false; + bool m_keyNavigationEnabled = false; + bool m_blockTargetWheel = true; + bool m_scrollFlickableTarget = true; + // Same as QXcbWindow. + constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier; + // Same as QScrollBar/QAbstractSlider. + constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = + Qt::ControlModifier | Qt::ShiftModifier; + Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers; + QTimer m_wheelScrollingTimer; + KirigamiWheelEvent m_kirigamiWheelEvent; + + // Smooth scrolling + QQmlEngine* m_engine = nullptr; + bool m_wasTouched = false; + + struct Flickable { + QQmlProperty originX, originY; + QQmlProperty leftMargin, rightMargin; + QQmlProperty topMargin, bottomMargin; + QQmlProperty contentX, contentY; + QQmlProperty contentHeight, contentWidth; + QQmlProperty height, width; + QQmlProperty interactive; + } m_flickable; + + struct ScrollBar { + QQmlProperty scrollBar; + QQmlProperty stepSize; + QMetaMethod increaseMethod; + QMetaMethod decreaseMethod; + QQuickItem* item = nullptr; + bool valid() const { return item != nullptr; }; + } m_scrollBarV, m_scrollBarH; + + bool m_useAnimation = false; +}; diff --git a/include/qml_material/round_item.h b/include/qml_material/round_item.h new file mode 100644 index 0000000..6f4839c --- /dev/null +++ b/include/qml_material/round_item.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace qml_material { + class RoundItem : public QQuickItem { + Q_OBJECT + QML_ELEMENT + public: + RoundItem(QQuickItem* parent = nullptr); + ~RoundItem() override; + + + protected: + QSGNode* updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; + }; +} \ No newline at end of file diff --git a/include/qml_material/theme.h b/include/qml_material/theme.h new file mode 100644 index 0000000..3be34fd --- /dev/null +++ b/include/qml_material/theme.h @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "qml_material/color.h" + +#define ATTACH_PROPERTY(_type_, _name_) \ +private: \ + Q_PROPERTY(_type_ _name_ READ _name_ WRITE set_##_name_ RESET reset_##_name_ NOTIFY \ + _name_##Changed FINAL) \ +public: \ + _type_ _name_() const; \ + void set_##_name_(_type_); \ + void reset_##_name_(); \ + AttachProp<_type_>& get_##_name_(); \ + Q_SIGNAL void _name_##Changed(); \ + \ +private: \ + AttachProp<_type_> m_##_name_ { &Self::_name_##Changed }; + +namespace qml_material +{ + +class Theme : public QQuickAttachedPropertyPropagator { + Q_OBJECT + + QML_NAMED_ELEMENT(MatProp) + QML_UNCREATABLE("") + QML_ATTACHED(Theme) + +public: + using Self = Theme; + + template + struct AttachProp { + using SigFunc = void (Theme::*)(); + using ReadFunc = V (Theme::*const)(); + using WriteFunc = void (Theme::*)(V); + using GetFunc = AttachProp& (Theme::*const)(); + + std::optional value; + bool explicited; + SigFunc sig_func; + + AttachProp(SigFunc s): value(), explicited(false), sig_func(s) {} + }; + + ATTACH_PROPERTY(QColor, textColor) + ATTACH_PROPERTY(QColor, supportTextColor) + ATTACH_PROPERTY(QColor, backgroundColor) + ATTACH_PROPERTY(QColor, stateLayerColor) + ATTACH_PROPERTY(int, elevation) + ATTACH_PROPERTY(MdColorMgr*, color) + +public: + Theme(QObject* parent); + ~Theme(); + + static Theme* qmlAttachedProperties(QObject* object); + +protected: + void attachedParentChange(QQuickAttachedPropertyPropagator* newParent, + QQuickAttachedPropertyPropagator* oldParent) override; +}; +} // namespace qml_material + +#undef ATTACH_PROPERTY \ No newline at end of file diff --git a/include/qml_material/type.h b/include/qml_material/type.h new file mode 100644 index 0000000..3454f81 --- /dev/null +++ b/include/qml_material/type.h @@ -0,0 +1,94 @@ +#pragma once +#include +#include + +namespace qml_material +{ +class Enum : public QObject { + Q_OBJECT + QML_ELEMENT +public: + using QObject::QObject; + + enum class ColorScheme + { + Light, + Dark + }; + Q_ENUM(ColorScheme) + + enum class IconStyle + { + IconRound = 0, + IconFilled + }; + Q_ENUM(IconStyle) + + enum class IconLabelStyle + { + IconAndText = 0, + IconOnly, + TextOnly + }; + Q_ENUM(IconLabelStyle) + + enum class ButtonType + { + BtElevated = 0, + BtFilled, + BtFilledTonal, + BtOutlined, + BtText + }; + Q_ENUM(ButtonType) + + enum class IconButtonType + { + IBtFilled = 0, + IBtFilledTonal, + IBtOutlined, + IBtStandard + }; + Q_ENUM(IconButtonType) + + enum class FABType + { + FABSmall = 0, + FABNormal, + FABLarge + }; + Q_ENUM(FABType) + + enum class FABColor + { + FABColorPrimary = 0, + FABColorSurfaec, + FABColorSecondary, + FABColorTertiary + }; + Q_ENUM(FABColor) + + enum class CardType + { + CardElevated = 0, + CardFilled, + CardOutlined + }; + Q_ENUM(CardType) + + enum class TextFieldType + { + TextFieldFilled = 0, + TextFieldOutlined + }; + Q_ENUM(TextFieldType) + + enum class ListItemHeightMode + { + ListItemOneLine = 0, + ListItemTwoLine, + ListItemThreeine + }; + Q_ENUM(ListItemHeightMode) +}; +} // namespace qml_material \ No newline at end of file diff --git a/include/qml_material/type_scale.h b/include/qml_material/type_scale.h new file mode 100644 index 0000000..a865c9f --- /dev/null +++ b/include/qml_material/type_scale.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include "core/core.h" + +namespace qml_material +{ + +struct TypeScaleItem { + Q_GADGET + QML_ELEMENT + QML_VALUE_TYPE(t_typescale) + + Q_PROPERTY(i32 size MEMBER size) + Q_PROPERTY(i32 line_height MEMBER line_height) + Q_PROPERTY(QFont::Weight weight MEMBER weight) + Q_PROPERTY(QFont::Weight weight_prominent MEMBER weight_prominent) + Q_PROPERTY(qreal tracking MEMBER tracking) +public: + Q_INVOKABLE TypeScaleItem fresh() const { return *this; } + + i32 size; + i32 line_height; + QFont::Weight weight; + QFont::Weight weight_prominent; + qreal tracking; +}; + +class TypeScale : public QObject { + Q_OBJECT + QML_ELEMENT +public: + using QObject::QObject; + +#define X(NAME, ...) \ + Q_PROPERTY(TypeScaleItem NAME READ NAME NOTIFY typescaleChanged) \ +public: \ + TypeScaleItem NAME() const { return m_##NAME; } \ + \ +private: \ + TypeScaleItem m_##NAME { __VA_ARGS__ }; + // clang-format off + X(display_large , 57, 64, QFont::Normal, QFont::Normal, -0.25) + X(display_medium , 45, 52, QFont::Normal, QFont::Normal, 0.0 ) + X(display_small , 36, 44, QFont::Normal, QFont::Normal, 0.0 ) + X(headline_large , 32, 40, QFont::Medium, QFont::Medium, 0.0 ) + X(headline_medium, 28, 36, QFont::Medium, QFont::Medium, 0.0 ) + X(headline_small , 24, 32, QFont::Medium, QFont::Medium, 0.0 ) + X(title_large , 22, 28, QFont::Normal, QFont::Normal, 0.0 ) + X(title_medium , 16, 24, QFont::Medium, QFont::Medium, 0.15 ) + X(title_small , 14, 20, QFont::Medium, QFont::Medium, 0.1 ) + X(body_large , 16, 24, QFont::Normal, QFont::Normal, 0.5 ) + X(body_medium , 14, 20, QFont::Normal, QFont::Normal, 0.25 ) + X(body_small , 12, 16, QFont::Normal, QFont::Normal, 0.4 ) + X(label_large , 14, 20, QFont::Medium, QFont::Bold , 0.1 ) + X(label_medium , 12, 16, QFont::Medium, QFont::Bold , 0.5 ) + X(label_small , 11, 16, QFont::Medium, QFont::Bold , 0.5 ) + // clang-format on + +#undef X + +Q_SIGNALS: + void typescaleChanged(); +}; + +} // namespace qml_material \ No newline at end of file diff --git a/include/qml_material/util.h b/include/qml_material/util.h new file mode 100644 index 0000000..ea7972c --- /dev/null +++ b/include/qml_material/util.h @@ -0,0 +1,107 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "qml_material/corner.h" +#include "qml_material/type.h" +#include "core/core.h" + +namespace qml_material +{ + +class Xdp : public QObject { + Q_OBJECT +public: + + Xdp(QObject* parent = nullptr); + ~Xdp(); + + static Xdp* insance(); + + QColor accentColor() const; + Qt::ColorScheme colorScheme() const; +public Q_SLOTS: + void xdpSettingChangeSlot(QString, QString, QDBusVariant); + +Q_SIGNALS: + void colorSchemeChanged(); + void accentColorChanged(); + +private: + std::optional m_color_scheme; + std::optional m_accent_color; +}; + +class Util : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON +public: + Util(QObject* parent = nullptr); + ~Util(); + + enum Track + { + TrackCreate = 0, + TrackDelete + }; + Q_ENUMS(Track) + + Q_INVOKABLE void track(QVariant, Track); + + Q_INVOKABLE bool hasIcon(const QJSValue& v) { + auto name = v.property("name"); + auto source = v.property("source"); + if (name.isString() && source.isVariant()) { + return ! name.toString().isEmpty() || ! source.toString().isEmpty(); + } + return false; + } + + Q_INVOKABLE QColor transparent(QColor in, float alpha) { + in.setAlphaF(alpha); + return in; + } + + Q_INVOKABLE void closePopup(QObject* obj) { + do { + auto meta = obj->metaObject(); + do { + if (meta->className() == std::string("QQuickPopup")) { + QMetaObject::invokeMethod(obj, "close"); + return; + } + } while (meta = meta->superClass(), meta); + } while (obj = obj->parent(), obj); + } + + Q_INVOKABLE QColor hoverColor(QColor in) { + in.setAlphaF(0.08); + return in; + } + + Q_INVOKABLE QColor pressColor(QColor in) { + in.setAlphaF(0.18); + return in; + } + + Q_INVOKABLE qreal devicePixelRatio(QQuickItem* in) { + return in ? in->window() ? in->window()->devicePixelRatio() : 1.0 : 1.0; + } + + // tl tr bl br + Q_INVOKABLE CornersGroup corner(QVariant in); + + Q_INVOKABLE CornersGroup corner(qreal br, qreal tr, qreal bl, qreal tl); + + QString type_str(const QJSValue&); + Q_INVOKABLE void print_parents(const QJSValue&); +private: + usize m_tracked { 0 }; +}; +} // namespace qml_material \ No newline at end of file diff --git a/qml/Button.qml b/qml/Button.qml new file mode 100644 index 0000000..604714c --- /dev/null +++ b/qml/Button.qml @@ -0,0 +1,200 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Button { + id: control + + property int type: MD.Enum.BtElevated + property int iconStyle: hasIcon ? MD.Enum.IconAndText : MD.Enum.TextOnly + readonly property bool hasIcon: MD.Util.hasIcon(icon) + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + flat: type == MD.Enum.BtText || type == MD.Enum.BtOutlined + topInset: 6 + bottomInset: 6 + verticalPadding: 14 + // https://m3.material.io/components/buttons/specs#256326ad-f934-40e7-b05f-0bcb41aa4382 + leftPadding: flat ? 12 : (hasIcon ? 16 : 24) + rightPadding: flat ? (hasIcon ? 16 : 12) : 24 + spacing: 8 + + icon.width: 24 + icon.height: 24 + + font.weight: MD.Token.typescale.label_large.weight + font.pointSize: MD.Token.typescale.label_large.size + property int lineHeight: MD.Token.typescale.label_large.line_height + + contentItem: MD.IconLabel { + lineHeight: control.lineHeight + + font: control.font + text: control.text + icon_style: control.iconStyle + + icon_name: control.icon.name + icon_size: control.icon.width + } + + background: Rectangle { + implicitWidth: 64 + implicitHeight: 40 + + radius: height / 2 + color: control.MD.MatProp.backgroundColor + + border.width: control.type == MD.Enum.BtOutlined ? 1 : 0 + border.color: control.MD.MatProp.color.outline + + // The layer is disabled when the button color is transparent so you can do + // Material.background: "transparent" and get a proper flat button without needing + // to set Material.elevation as well + layer.enabled: control.enabled && color.a > 0 && !control.flat + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + elevation: MD.Token.elevation.level1 + textColor: { + switch (control.type) { + case MD.Enum.BtFilled: + case MD.Enum.BtFilledTonal: + return ctx.color.getOn(control.MD.MatProp.backgroundColor); + case MD.Enum.BtOutlined: + case MD.Enum.BtText: + case MD.Enum.BtElevated: + default: + return ctx.color.primary; + } + } + backgroundColor: { + switch (control.type) { + case MD.Enum.BtFilled: + return ctx.color.primary; + case MD.Enum.BtFilledTonal: + return ctx.color.secondary_container; + case MD.Enum.BtOutlined: + case MD.Enum.BtText: + return ctx.color.surface; + case MD.Enum.BtElevated: + default: + return ctx.color.surface_container_low; + } + } + stateLayerColor: "transparent" + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.elevation: MD.Token.elevation.level0 + item_state.textColor: item_state.ctx.color.on_surface + item_state.backgroundColor: item_state.ctx.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Pressed" + when: control.down + PropertyChanges { + item_state.elevation: MD.Token.elevation.level1 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = null; + switch (control.type) { + case MD.Enum.BtFilled: + case MD.Enum.BtFilledTonal: + c = item_state.ctx.color.getOn(item_state.ctx.backgroundColor); + break; + case MD.Enum.BtOutlined: + case MD.Enum.BtText: + case MD.Enum.BtElevated: + default: + c = item_state.ctx.color.primary; + } + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.elevation: MD.Token.elevation.level2 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = null; + switch (control.type) { + case MD.Enum.BtFilled: + case MD.Enum.BtFilledTonal: + c = item_state.ctx.color.getOn(item_state.ctx.backgroundColor); + break; + case MD.Enum.BtOutlined: + case MD.Enum.BtText: + case MD.Enum.BtElevated: + default: + c = item_state.ctx.color.primary; + } + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + }, + State { + name: "Focus" + when: control.focus + PropertyChanges { + item_state.elevation: MD.Token.elevation.level1 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = null; + switch (control.type) { + case MD.Enum.BtFilled: + case MD.Enum.BtFilledTonal: + c = item_state.ctx.color.getOn(item_state.ctx.backgroundColor); + break; + case MD.Enum.BtOutlined: + case MD.Enum.BtText: + case MD.Enum.BtElevated: + default: + c = item_state.ctx.color.primary; + } + return MD.Util.transparent(c, MD.Token.state.focus.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/Card.qml b/qml/Card.qml new file mode 100644 index 0000000..e45d3f0 --- /dev/null +++ b/qml/Card.qml @@ -0,0 +1,159 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Button { + id: control + + property int type: MD.Enum.CardElevated + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + + verticalPadding: 0 + horizontalPadding: 16 + + contentItem: Item { + } + + background: Rectangle { + implicitWidth: 64 + implicitHeight: 64 + + radius: 12 + color: MD.MatProp.backgroundColor + + border.width: control.type == MD.Enum.CardOutlined ? 1 : 0 + border.color: item_state.ctx.color.outline + + layer.enabled: control.enabled && color.a > 0 && !control.flat + layer.effect: MD.RoundedElevationEffect { + elevation: MD.MatProp.elevation + } + } + + Binding { + control.contentItem.z: -1 + control.background.z: -2 + when: control.contentItem + } + + MD.Ripple { + clip: true + clipRadius: 12 + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.pressed || control.visualFocus || control.hovered) + color: MD.MatProp.stateLayerColor + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + elevation: { + switch (control.type) { + case MD.Enum.CardOutlined: + case MD.Enum.CardFilled: + return MD.Token.elevation.level0; + case MD.Enum.CardElevated: + default: + return MD.Token.elevation.level1; + } + } + textColor: item_state.ctx.color.getOn(backgroundColor) + backgroundColor: { + switch (control.type) { + case MD.Enum.CardOutlined: + return item_state.ctx.color.surface; + case MD.Enum.CardFilled: + return item_state.ctx.color.surface_container_highest; + case MD.Enum.CardElevated: + default: + return item_state.ctx.color.surface_container_low; + } + } + stateLayerColor: "transparent" + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.elevation: { + switch (control.type) { + case MD.Enum.CardFilled: + return MD.Token.elevation.level1; + case MD.Enum.CardOutlined: + case MD.Enum.CardElevated: + default: + return MD.Token.elevation.level0; + } + } + item_state.backgroundColor: item_state.ctx.color.surface_variant + control.contentItem.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.elevation: { + switch (control.type) { + case MD.Enum.CardOutlined: + case MD.Enum.CardFilled: + return MD.Token.elevation.level0; + case MD.Enum.CardElevated: + default: + return MD.Token.elevation.level1; + } + } + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.elevation: { + switch (control.type) { + case MD.Enum.CardOutlined: + case MD.Enum.CardFilled: + return MD.Token.elevation.level1; + case MD.Enum.CardElevated: + default: + return MD.Token.elevation.level2; + } + } + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/CircularIndicator.qml b/qml/CircularIndicator.qml new file mode 100644 index 0000000..b6a44a2 --- /dev/null +++ b/qml/CircularIndicator.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.Material.impl as MDImpl + +import Qcm.Material as MD + +T.BusyIndicator { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + padding: 6 + + contentItem: MDImpl.BusyIndicatorImpl { + implicitWidth: 48 + implicitHeight: 48 + color: control.MD.MatProp.color.primary + + running: control.running + opacity: control.running ? 1 : 0 + Behavior on opacity { OpacityAnimator { duration: 250 } } + } +} \ No newline at end of file diff --git a/qml/ComboBox.qml b/qml/ComboBox.qml new file mode 100644 index 0000000..43f07c7 --- /dev/null +++ b/qml/ComboBox.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls.impl +import QtQuick.Templates as T +import QtQuick.Controls.Material.impl as MDImpl +import Qcm.Material as MD + +T.ComboBox { + id: control + + property int type: MD.Enum.TextFieldOutlined + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding) + + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + + padding: 12 + leftPadding: padding + (!control.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing) + rightPadding: padding + (control.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing) + topPadding: 0 + bottomPadding: 0 + + delegate: MD.MenuItem { + required property var model + required property int index + + width: ListView.view.width + text: model[control.textRole] + // Material.foreground: control.currentIndex === index ? ListView.view.contentItem.Material.accent : ListView.view.contentItem.Material.foreground + highlighted: control.highlightedIndex === index + hoverEnabled: true//control.hoverEnabled + } + + indicator: MD.Icon { + x: control.mirrored ? control.padding : control.width - width - control.padding + y: control.topPadding + (control.availableHeight - height) / 2 + name: MD.Token.icon.arrow_drop_down + size: 24 + } + + contentItem: MD.TextInput { + typescale: MD.Token.typescale.body_large + + padding: 0 + text: control.editable ? control.editText : control.displayText + enabled: control.editable + autoScroll: control.editable + readOnly: !control.editable + inputMethodHints: control.inputMethodHints + validator: control.validator + selectByMouse: control.selectTextByMouse + color: item_state.textColor + selectionColor: item_state.ctx.color.primary + selectedTextColor: item_state.ctx.color.getOn(selectionColor) + verticalAlignment: TextInput.AlignVCenter + } + background: MDImpl.MaterialTextContainer { + implicitWidth: 64 + implicitHeight: 56 + + filled: control.type === MD.Enum.TextFieldFilled + fillColor: control.MD.MatProp.backgroundColor + outlineColor: control.outlineColor + focusedOutlineColor: control.outlineColor + controlHasActiveFocus: control.activeFocus + controlHasText: true + horizontalPadding: 16 + } + + popup: MD.Menu { + y: control.editable ? control.height - 5 : 0 + height: Math.min(contentItem.implicitHeight + verticalPadding * 2, control.Window.height - topMargin - bottomMargin) + width: control.width + transformOrigin: Item.Top + modal: false + model: control.delegateModel + topMargin: 12 + bottomMargin: 12 + verticalPadding: 8 + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + property color outlineColor: item_state.outlineColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level0 + textColor: item_state.ctx.color.on_surface + backgroundColor: "transparent" + supportTextColor: item_state.ctx.color.on_surface_variant + property color outlineColor: item_state.ctx.color.outline + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.supportTextColor: item_state.ctx.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Error" + when: !control.acceptableInput && !control.hovered + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: item_state.ctx.color.error + item_state.outlineColor: item_state.ctx.color.error + } + }, + State { + name: "ErrorHover" + when: !control.acceptableInput && control.hovered + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: item_state.ctx.color.error + item_state.outlineColor: item_state.ctx.color.on_error_container + } + }, + State { + name: "Focused" + when: control.focus + PropertyChanges { + item_state.outlineColor: item_state.ctx.color.primary + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.outlineColor: item_state.ctx.color.on_surface + } + } + ] + } +} diff --git a/qml/Control.qml b/qml/Control.qml new file mode 100644 index 0000000..2ce09d4 --- /dev/null +++ b/qml/Control.qml @@ -0,0 +1,10 @@ +import QtQuick +import QtQuick.Templates as T + +T.Control { + focusPolicy: Qt.NoFocus + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) +} \ No newline at end of file diff --git a/qml/Dialog.qml b/qml/Dialog.qml new file mode 100644 index 0000000..0892d2b --- /dev/null +++ b/qml/Dialog.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Dialog { + id: control + + property int titleCapitalization: Font.Capitalize + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding, implicitHeaderWidth, implicitFooterWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + verticalPadding: 24 + + modal: true + clip: false + + enter: Transition { + // grow_fade_in + NumberAnimation { + property: "scale" + from: 0.9 + to: 1.0 + easing.type: Easing.OutQuint + duration: 220 + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + easing.type: Easing.OutCubic + duration: 150 + } + } + + exit: Transition { + // shrink_fade_out + NumberAnimation { + property: "scale" + from: 1.0 + to: 0.9 + easing.type: Easing.OutQuint + duration: 220 + } + NumberAnimation { + property: "opacity" + from: 1.0 + to: 0.0 + easing.type: Easing.OutCubic + duration: 150 + } + } + + background: Rectangle { + // FullScale doesn't make sense for Dialog. + radius: MD.Token.shape.corner.extra_large + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.MD.MatProp.elevation + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + roundedScale: control.background.radius + } + } + + header: MD.Control { + topPadding: 24 + horizontalPadding: 24 + contentItem: ColumnLayout { + MD.Text { + visible: control.title + text: control.title + typescale: MD.Token.typescale.headline_small + color: item_state.ctx.color.on_surface + font.capitalization: control.titleCapitalization + } + } + } + + footer: Item { + } + + // DialogButtonBox { + // visible: count > 0 + // } + + T.Overlay.modal: Rectangle { + color: MD.Util.transparent(item_state.ctx.color.scrim, 0.32) + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } + + T.Overlay.modeless: Rectangle { + color: MD.Util.transparent(item_state.ctx.color.scrim, 0.32) + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level3 + textColor: item_state.ctx.color.primary + backgroundColor: item_state.ctx.color.surface_container_high + supportTextColor: item_state.ctx.color.on_surface_variant + } +} diff --git a/qml/Divider.qml b/qml/Divider.qml new file mode 100644 index 0000000..8d31b30 --- /dev/null +++ b/qml/Divider.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Layouts + +import Qcm.Material as MD + +Rectangle { + id: root + property bool full: true + property int orientation: { + if(parent instanceof RowLayout) + return Qt.Vertical + else return Qt.Horizontal + } + property int size: 1 + + Binding { + root.Layout.fillWidth: true + root.implicitHeight: root.size + root.height: root.size + when: full && isLayout(root.parent) && orientation === Qt.Horizontal + } + + Binding { + root.anchors.left: root.parent.left + root.anchors.right: root.parent.right + root.implicitHeight: root.size + root.height: root.size + when: full && !isLayout(root.parent) && orientation === Qt.Horizontal + } + + Binding { + root.Layout.fillHeight: true + root.implicitWidth: root.size + root.width: root.size + when: full && isLayout(root.parent) && orientation !== Qt.Horizontal + } + + Binding { + root.anchors.top: root.parent.top + root.anchors.bottom: root.parent.bottom + root.implicitWidth: root.size + root.width: root.size + when: full && !isLayout(root.parent) && orientation !== Qt.Horizontal + } + + function isLayout(p) { + return (p instanceof RowLayout) || (p instanceof GridLayout) || (p instanceof ColumnLayout); + } + color: MD.Token.color.outline_variant +} \ No newline at end of file diff --git a/qml/Drawer.qml b/qml/Drawer.qml new file mode 100644 index 0000000..e6e6ca1 --- /dev/null +++ b/qml/Drawer.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl +import Qcm.Material as MD + +T.Drawer { + id: control + + parent: T.Overlay.overlay + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + topPadding: edge !== Qt.TopEdge ? 16 : 0 + bottomPadding: edge !== Qt.BottomEdge ? 16 : 0 + + enter: Transition { + SmoothedAnimation { + velocity: 5 + } + } + exit: Transition { + SmoothedAnimation { + velocity: 5 + } + } + + MD.MatProp.elevation: !interactive && !dim ? MD.Token.elevation.level0 : MD.Token.elevation.level1 + + background: Item { + implicitWidth: 200 + Rectangle { + anchors.fill: parent + color: MD.Token.color.surface + layer.enabled: true + layer.effect: MD.RoundClip { + radius: [0, 16, 0, 16] + size: parent.height + } + } + //layer.enabled: control.position > 0 && control.MD.MatProp.elevation > 0 + //layer.effect: MD.RoundedElevationEffect { + // elevation: control.MD.MatProp.elevation + //} + } + + T.Overlay.modal: Rectangle { + color: MD.Util.transparent(MD.Token.color.scrim, 0.32) + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } + + T.Overlay.modeless: Rectangle { + color: MD.Util.transparent(MD.Token.color.scrim, 0.32) + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } +} diff --git a/qml/DrawerItem.qml b/qml/DrawerItem.qml new file mode 100644 index 0000000..0943f2a --- /dev/null +++ b/qml/DrawerItem.qml @@ -0,0 +1,134 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.ItemDelegate { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + + verticalPadding: 0 + leftPadding: 16 + rightPadding: 24 + spacing: 0 + checked: false + + icon.width: 24 + icon.height: 24 + + property alias trailing: item_holder_trailing.contentItem + + contentItem: RowLayout { + spacing: 12 + + MD.Icon { + id: item_holder_leader + name: control.icon.name + size: Math.min(control.icon.width, control.icon.height) + } + MD.Text { + Layout.fillWidth: true + font: control.font + text: control.text + typescale: MD.Token.typescale.label_large + maximumLineCount: control.maximumLineCount + verticalAlignment: Qt.AlignVCenter + } + MD.Control { + id: item_holder_trailing + Layout.alignment: Qt.AlignVCenter + visible: contentItem + } + } + + background: Rectangle { + implicitWidth: 336 + implicitHeight: 56 + + radius: 28 + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.enabled && color.a > 0 + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + } + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + visible: false + + elevation: MD.Token.elevation.level0 + textColor: control.checked ? on_secondary_container : MD.Token.color.on_surface_variant + backgroundColor: control.checked ? MD.Token.color.secondary_container : "transparent" + supportTextColor: MD.Token.color.on_surface_variant + stateLayerColor: "transparent" + + states: [ + State { + name: "Disabled" + when: !control.enabled + PropertyChanges { + item_state.elevation: MD.Token.elevation.level0 + item_state.textColor: MD.Token.color.on_surface + item_state.supportTextColor: MD.Token.color.on_surface + item_state.backgroundColor: MD.Token.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.38 + } + }, + State { + name: "Hovered" + when: control.enabled && control.hovered && !control.down + PropertyChanges { + item_state.textColor: control.checked ? on_secondary_container : MD.Token.color.on_surface + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = control.checked ? MD.Token.color.on_secondary_container : MD.Token.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + }, + State { + name: "Pressed" + when: control.enabled && control.down + PropertyChanges { + item_state.textColor: control.checked ? on_secondary_container : MD.Token.color.on_surface + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = MD.Token.color.on_secondary_container; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/FAB.qml b/qml/FAB.qml new file mode 100644 index 0000000..a280e9b --- /dev/null +++ b/qml/FAB.qml @@ -0,0 +1,154 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Button { + id: control + + property int type: MD.Enum.FABNormal + property int color: MD.Enum.FABColorPrimary + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + flat: false + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + + padding: _size(type, 8, 16, 30) + spacing: 0 + + icon.width: _size(type, 24, 24, 36) + icon.height: _size(type, 24, 24, 36) + + font.weight: MD.Token.typescale.label_large.weight + font.pixelSize: Math.min(icon.width, icon.height) + font.family: MD.Token.font.icon_round.family + + contentItem: Item { + implicitWidth: control.icon.width + implicitHeight: control.icon.height + Text { + anchors.centerIn: parent + font: control.font + text: control.icon.name + color: MD.MatProp.textColor + lineHeight: font.pixelSize + lineHeightMode: Text.FixedHeight + } + } + + background: Rectangle { + implicitWidth: _size(type, 40, 56, 96) + implicitHeight: _size(type, 40, 56, 96) + + radius: _size(type, 12, 16, 28) + color: MD.MatProp.backgroundColor + + border.width: control.type == MD.Enum.BtOutlined ? 1 : 0 + border.color: item_state.ctx.color.outline + + // The layer is disabled when the button color is transparent so you can do + // Material.background: "transparent" and get a proper flat button without needing + // to set Material.elevation as well + layer.enabled: control.enabled && color.a > 0 && !control.flat + layer.effect: MD.RoundedElevationEffect { + elevation: MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: MD.MatProp.stateLayerColor + } + } + + function _size(t, small, normal, large) { + return t == MD.Enum.FABSmall ? small : (t == MD.Enum.FABLarge ? large : normal); + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level3 + textColor: { + switch (control.color) { + case MD.Enum.FABColorSurfaec: + return item_state.ctx.color.primary; + case MD.Enum.FABColorSecondary: + return item_state.ctx.color.on_secondary_container; + case MD.Enum.FABColorTertiary: + return item_state.ctx.color.on_tertiary_container; + case MD.Enum.FABColorPrimary: + default: + return item_state.ctx.color.on_primary_container; + } + } + backgroundColor: { + switch (control.color) { + case MD.Enum.FABColorSurfaec: + return item_state.ctx.color.surface_container_high; + case MD.Enum.FABColorSecondary: + return item_state.ctx.color.secondary_container; + case MD.Enum.FABColorTertiary: + return item_state.ctx.color.tertiary_container; + case MD.Enum.FABColorPrimary: + default: + return item_state.ctx.color.primary_container; + } + } + stateLayerColor: "transparent" + + states: [ + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.elevation: MD.Token.elevation.level3 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.textColor; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.elevation: MD.Token.elevation.level4 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.textColor; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/Flickable.qml b/qml/Flickable.qml new file mode 100644 index 0000000..0fec20e --- /dev/null +++ b/qml/Flickable.qml @@ -0,0 +1,29 @@ +import QtQuick + +import Qcm.Material as MD + +Flickable { + id: root + + clip: true + contentHeight: contentItem.childrenRect.height + contentWidth: width - rightMargin - leftMargin + implicitHeight: contentHeight + topMargin + bottomMargin + implicitWidth: contentWidth + rightMargin + leftMargin + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 + + signal wheelMoved + + MD.WheelHandler { + id: wheel + target: root + filterMouseEvents: false + onWheelMoved: root.wheelMoved() + } + ScrollBar.vertical: MD.ScrollBar { + } +} + diff --git a/qml/FontMetrics.qml b/qml/FontMetrics.qml new file mode 100644 index 0000000..efbaa4e --- /dev/null +++ b/qml/FontMetrics.qml @@ -0,0 +1,13 @@ +import QtQuick + +import Qcm.Material as MD + +FontMetrics { + id: root + property MD.t_typescale typescale: MD.Token.typescale.label_medium + property bool prominent: false + + font.pixelSize: typescale?.size ?? 16 + font.weight: typescale ? (root.prominent && typescale.weight_prominent ? typescale.weight_prominent : typescale.weight) : Font.Normal + font.letterSpacing: typescale?.tracking ?? 1 +} \ No newline at end of file diff --git a/qml/GridView.qml b/qml/GridView.qml new file mode 100644 index 0000000..50a9ddc --- /dev/null +++ b/qml/GridView.qml @@ -0,0 +1,17 @@ +import QtQuick +import QtQuick.Controls +import Qcm.Material as MD + +GridView { + id: root + + signal wheelMoved + MD.WheelHandler { + id: wheel + target: root + filterMouseEvents: false + onWheelMoved: root.wheelMoved() + } + ScrollBar.vertical: MD.ScrollBar { + } +} diff --git a/qml/Icon.qml b/qml/Icon.qml new file mode 100644 index 0000000..5de2b63 --- /dev/null +++ b/qml/Icon.qml @@ -0,0 +1,37 @@ +import QtQuick +import QtQuick.Layouts +import Qcm.Material as MD + +Item { + id: root + + implicitWidth: size + implicitHeight: size + + property string name + property int size: 24 + property alias horizontalAlignment: item_text_icon.horizontalAlignment + + property int lineHeight: MD.Token.typescale.label_large.line_height + property int iconStyle: MD.Enum.IconRound + property color color: MD.MatProp.textColor + + Text { + id: item_text_icon + anchors.centerIn: parent + + font.family: { + switch (root.iconStyle) { + case MD.Enum.IconRound: + default: + return MD.Token.font.icon_round.family; + } + } + + font.pixelSize: root.size + text: root.name + color: root.color + lineHeight: root.lineHeight + lineHeightMode: Text.FixedHeight + } +} diff --git a/qml/IconButton.qml b/qml/IconButton.qml new file mode 100644 index 0000000..8370f11 --- /dev/null +++ b/qml/IconButton.qml @@ -0,0 +1,218 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Button { + id: control + + property int type: MD.Enum.IBtStandard + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + flat: type == MD.Enum.IBtStandard || (type == MD.Enum.IBtOutlined && !control.checked) + topInset: 4 + bottomInset: 4 + leftInset: 4 + rightInset: 4 + + padding: 8 + spacing: 0 + + icon.width: 24 + icon.height: 24 + + contentItem: Item { + implicitWidth: control.icon.width + implicitHeight: control.icon.height + MD.Icon { + anchors.centerIn: parent + name: icon.name + size: Math.min(icon.width, icon.height) + } + } + + background: Rectangle { + implicitWidth: 40 + implicitHeight: 40 + + radius: height / 2 + color: control.MD.MatProp.backgroundColor + + border.width: control.type == MD.Enum.IBtOutlined ? 1 : 0 + border.color: item_state.ctx.color.outline + + layer.enabled: control.enabled && color.a > 0 && !control.flat + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level1 + stateLayerColor: "transparent" + + textColor: { + switch (control.type) { + case MD.Enum.iBtOutlined: + if (control.checked) + return item_state.ctx.color.on_inverse_surface; + else + return item_state.ctx.color.on_surface_variant; + case MD.Enum.IBtStandard: + if (control.checked) + return item_state.ctx.color.primary; + else + return item_state.ctx.color.on_surface_variant; + case MD.Enum.IBtFilledTonal: + if (!control.checkable || control.checked) + return item_state.ctx.color.on_secondary_container; + else + return item_state.ctx.color.on_surface_variant; + case MD.Enum.IBtFilled: + default: + if (!control.checkable || control.checked) + return item_state.ctx.color.on_primary; + else + return item_state.ctx.color.primary; + } + } + backgroundColor: { + switch (control.type) { + case MD.Enum.IBtStandard: + return "transparent"; + case MD.Enum.IBtOutlined: + if (control.checked) + return item_state.ctx.color.inverse_surface2; + else + return "transparent"; + case MD.Enum.IBtFilledTonal: + if (!control.checkable || control.checked) + return item_state.ctx.color.secondary_container; + else + return item_state.ctx.color.surface_container_highest; + case MD.Enum.IBtFilled: + default: + if (!control.checkable || control.checked) + return item_state.ctx.color.primary; + else + return item_state.ctx.color.surface_container_highest; + } + } + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.elevation: MD.Token.elevation.level0 + item_state.textColor: item_state.ctx.color.on_surface + item_state.backgroundColor: item_state.ctx.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.elevation: MD.Token.elevation.level1 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = null; + switch (control.type) { + case MD.Enum.IBtFilled: + if (!control.checkable || control.checked) + c = item_state.ctx.color.on_primary; + else + c = item_state.ctx.color.primary; + break; + case MD.Enum.IBtFilledTonal: + if (!control.checkable || control.checked) + c = item_state.ctx.color.on_secondary_container; + else + c = item_state.ctx.color.on_surface_variant; + break; + case MD.Enum.IBtOutlined: + if (control.checked) + c = item_state.ctx.color.on_inverse_surface; + else + c = item_state.ctx.color.on_surface; + break; + case MD.Enum.IBtStandard: + default: + if (control.checked) + c = item_state.ctx.color.primary; + else + c = item_state.ctx.color.on_surface_variant; + } + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.elevation: MD.Token.elevation.level2 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + let c = null; + switch (control.type) { + case MD.Enum.IBtFilled: + if (!control.checkable || control.checked) + c = item_state.ctx.color.on_primary; + else + c = item_state.ctx.color.primary; + break; + case MD.Enum.IBtFilledTonal: + if (!control.checkable || control.checked) + c = item_state.ctx.color.on_secondary_container; + else + c = item_state.ctx.color.on_surface_variant; + break; + case MD.Enum.IBtOutlined: + if (control.checked) + c = item_state.ctx.color.on_inverse_surface; + else + c = item_state.ctx.color.on_surface_variant; + break; + default: + case MD.Enum.IBtStandard: + if (control.checked) + c = item_state.ctx.color.primary; + else + c = item_state.ctx.color.on_surface_variant; + break; + } + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/IconLabel.qml b/qml/IconLabel.qml new file mode 100644 index 0000000..8976c8c --- /dev/null +++ b/qml/IconLabel.qml @@ -0,0 +1,60 @@ +import QtQuick +import QtQuick.Layouts +import Qcm.Material as MD + +Item { + id: root + + implicitWidth: layout_row.implicitWidth + implicitHeight: layout_row.implicitHeight + + property alias spacing: layout_row.spacing + + property alias text: item_label_text.text + property alias font: item_label_text.font + property color color: MD.MatProp.textColor + + property alias icon_name: item_label_icon.text + property alias icon_size: item_label_icon.font.pixelSize + property color icon_color: MD.MatProp.textColor + + property int lineHeight: MD.Token.typescale.label_large.line_height + property int icon_style: MD.Enum.IconAndText + + property int horizontalAlignment: Qt.AlignHCenter + + RowLayout { + id: layout_row + + anchors.fill: parent + spacing: 8 + + Text { + id: item_label_icon + Layout.alignment: root.horizontalAlignment | Qt.AlignVCenter + visible: root.icon_style != MD.Enum.TextOnly && text.length > 0 + + font.family: MD.Token.font.icon_round.family + color: root.icon_color + + lineHeight: root.lineHeight + lineHeightMode: Text.FixedHeight + } + + Text { + id: item_label_text + Layout.alignment: root.horizontalAlignment | Qt.AlignVCenter + + visible: root.icon_style != MD.Enum.IconOnly + color: root.color + lineHeight: root.lineHeight + lineHeightMode: Text.FixedHeight + } + + Item { + Layout.fillWidth: true + visible: root.horizontalAlignment == Qt.AlignLeft + } + + } +} \ No newline at end of file diff --git a/qml/Image.qml b/qml/Image.qml new file mode 100644 index 0000000..d7435b8 --- /dev/null +++ b/qml/Image.qml @@ -0,0 +1,55 @@ +import QtQuick +import Qcm.Material as MD + +Item { + id: root + // tl,tr,bl,br + property var radius: [0] + property alias asynchronous: image.asynchronous + property alias autoTransform: image.autoTransform + property alias cache: image.cache + property alias currentFrame: image.currentFrame + property alias fillMode: image.fillMode + property alias frameCount: image.frameCount + property alias horizontalAlignment: image.horizontalAlignment + property alias mipmap: image.mipmap + property alias mirror: image.mirror + property alias mirrorVertically: image.mirrorVertically + property alias progress: image.progress + property alias source: image.source + property alias sourceClipRect: image.sourceClipRect + property alias sourceSize: image.sourceSize + property alias status: image.status + property alias verticalAlignment: image.verticalAlignment + + property alias paintedHeight: image.paintedHeight + property alias paintedWidth: image.paintedWidth + + implicitHeight: { + return sourceSize.height; + } + implicitWidth: { + return sourceSize.width; + } + + MD.MatProp.elevation: MD.Token.elevation.level0 + layer.enabled: root.status === Image.Ready && root.paintedHeight > 0 && root.MD.MatProp.elevation != MD.Token.elevation.level0 + layer.effect: MD.RoundedElevationEffect { + elevation: root.MD.MatProp.elevation + } + + Image { + id: image + anchors.fill: parent + cache: true + smooth: true + fillMode: Image.PreserveAspectCrop + + + layer.enabled: true + layer.effect: MD.RoundClip { + radius: root.radius + size: root.height + } + } +} \ No newline at end of file diff --git a/qml/Label.qml b/qml/Label.qml new file mode 100644 index 0000000..1938171 --- /dev/null +++ b/qml/Label.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T + +import Qcm.Material as MD + +T.Label { + id: control + + linkColor: control.palette.link + + property MD.t_typescale typescale: MD.Token.typescale.label_medium + + Binding { + when: typescale + root.lineHeight: typescale.line_height + root.font.pixelSize: typescale.size + root.font.weight: typescale.weight + root.font.letterSpacing: typescale.tracking + } + + color: MD.MatProp.textColor + lineHeightMode: Text.FixedHeight + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 1 + textFormat: Text.PlainText +} \ No newline at end of file diff --git a/qml/ListItem.qml b/qml/ListItem.qml new file mode 100644 index 0000000..741abc8 --- /dev/null +++ b/qml/ListItem.qml @@ -0,0 +1,200 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.ItemDelegate { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + + verticalPadding: 8 + leftPadding: 16 + rightPadding: trailing ? 16 : 24 + spacing: 0 + + icon.width: 24 + icon.height: 24 + + readonly property int count: ListView.view?.count ?? 0 + readonly property int index_: index ? index : (model ? model.index : 0) + + property string supportText + property int maximumLineCount: 1 + property alias leader: item_holder_leader.contentItem + property alias trailing: item_holder_trailing.contentItem + property alias below: item_holder_below.contentItem + property alias divider: holder_divider.item + + property int heightMode: { + if (supportText) + return MD.Enum.ListItemTwoLine; + else + return MD.Enum.ListItemOneLine; + } + + contentItem: ColumnLayout { + RowLayout { + spacing: 16 + + MD.Control { + id: item_holder_leader + visible: contentItem + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 0 + MD.Text { + Layout.fillWidth: true + text: control.text + typescale: MD.Token.typescale.body_large + maximumLineCount: control.maximumLineCount + verticalAlignment: Qt.AlignVCenter + } + MD.Text { + Layout.fillWidth: true + visible: text + text: control.supportText + color: MD.MatProp.supportTextColor + typescale: MD.Token.typescale.body_medium + verticalAlignment: Qt.AlignVCenter + } + } + MD.Text { + id: item_text_trailing_support + Layout.alignment: Qt.AlignVCenter + visible: text + typescale: MD.Token.typescale.label_small + verticalAlignment: Qt.AlignVCenter + } + + MD.Control { + id: item_holder_trailing + Layout.alignment: Qt.AlignVCenter + visible: contentItem + } + + MD.Icon { + id: item_text_trailing_icon + Layout.alignment: Qt.AlignVCenter + visible: name.length + size: 24 + } + } + + RowLayout { + spacing: 16 + Item { + implicitWidth: item_holder_leader.height + visible: item_holder_leader.visible + } + + MD.Control { + id: item_holder_below + Layout.fillWidth: true + visible: contentItem + } + } + } + + background: Rectangle { + implicitWidth: 64 + implicitHeight: { + switch (control.heightMode) { + case MD.Enum.ListItemThreeLine: + return 96; + case MD.Enum.ListItemTwoLine: + return 72; + case MD.Enum.ListItemOneLine: + default: + return 56; + } + } + + radius: 0 + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.enabled && color.a > 0 + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + + MD.ItemHolder { + id: holder_divider + visible: control.index_ + 1 !== control.count + } + } + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level0 + textColor: item_state.ctx.color.on_surface + backgroundColor: item_state.ctx.color.surface + supportTextColor: item_state.ctx.color.on_surface_variant + stateLayerColor: "transparent" + + states: [ + State { + name: "Disabled" + when: !control.enabled + PropertyChanges { + item_state.elevation: MD.Token.elevation.level0 + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: item_state.ctx.color.on_surface + item_state.backgroundColor: item_state.ctx.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.38 + } + }, + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/ListView.qml b/qml/ListView.qml new file mode 100644 index 0000000..834c896 --- /dev/null +++ b/qml/ListView.qml @@ -0,0 +1,25 @@ +import QtQuick +import Qcm.Material as MD + +ListView { + id: root + + property bool busy: false + + footer: MD.ListBusyFooter { + running: root.busy + width: ListView.view.width + } + + signal wheelMoved + + MD.WheelHandler { + id: wheel + target: root + filterMouseEvents: false + onWheelMoved: root.wheelMoved() + } + + ScrollBar.vertical: MD.ScrollBar { + } +} diff --git a/qml/Menu.qml b/qml/Menu.qml new file mode 100644 index 0000000..29df2f0 --- /dev/null +++ b/qml/Menu.qml @@ -0,0 +1,86 @@ + +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import QtQuick.Window + +import Qcm.Material as MD + +T.Menu { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + margins: 0 + verticalPadding: 8 + + transformOrigin: !cascade ? Item.Top : (mirrored ? Item.TopRight : Item.TopLeft) + + delegate: MD.MenuItem {} + + property var model: contentModel + + enter: Transition { + // grow_fade_in + NumberAnimation { property: "scale"; from: 0.9; to: 1.0; easing.type: Easing.OutQuint; duration: 220 } + NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; easing.type: Easing.OutCubic; duration: 150 } + } + + exit: Transition { + // shrink_fade_out + NumberAnimation { property: "scale"; from: 1.0; to: 0.9; easing.type: Easing.OutQuint; duration: 220 } + NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; easing.type: Easing.OutCubic; duration: 150 } + } + + contentItem: ListView { + implicitHeight: contentHeight + + model: control.model + interactive: Window.window + ? contentHeight + control.topPadding + control.bottomPadding > Window.window.height + : false + clip: true + currentIndex: control.currentIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 48 + radius: MD.Token.shape.corner.extra_small + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.MD.MatProp.elevation > 0 + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + } + + T.Overlay.modal: Rectangle { + color: MD.Util.transparent(MD.Token.color.scrim, 0.32) + Behavior on opacity { NumberAnimation { duration: 150 } } + } + + T.Overlay.modeless: Rectangle { + color: MD.Util.transparent(MD.Token.color.scrim, 0.32) + Behavior on opacity { NumberAnimation { duration: 150 } } + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + visible: false + + elevation: MD.Token.elevation.level2 + textColor: MD.Token.color.on_surface + backgroundColor: MD.Token.color.surface_container + supportTextColor: MD.Token.color.on_surface_variant + } +} \ No newline at end of file diff --git a/qml/MenuItem.qml b/qml/MenuItem.qml new file mode 100644 index 0000000..c1df1d0 --- /dev/null +++ b/qml/MenuItem.qml @@ -0,0 +1,129 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.MenuItem { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding) + + padding: 16 + verticalPadding: 0 + spacing: 16 + + icon.width: 24 + icon.height: 24 + icon.color: MD.MatProp.textColor + + /* + indicator: CheckIndicator { + x: control.text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 + y: control.topPadding + (control.availableHeight - height) / 2 + visible: control.checkable + control: control + checkState: control.checked ? Qt.Checked : Qt.Unchecked + } + */ + + arrow: MD.Icon { + x: control.mirrored ? control.padding : control.width - width - control.padding + y: control.topPadding + (control.availableHeight - height) / 2 + + visible: control.subMenu + size: 24 + name: MD.Token.icon.arrow_right + } + + contentItem: MD.IconLabel { + + //readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0 + //readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0 + //leftPadding: !control.mirrored ? indicatorPadding : arrowPadding + //rightPadding: control.mirrored ? indicatorPadding : arrowPadding + + horizontalAlignment: Qt.AlignLeft + spacing: control.spacing + + text: control.text + font: control.font + icon_name: control.icon.name + icon_size: control.icon.width + icon_color: control.leadingIconColor + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 48 + color: "transparent" + + MD.Ripple { + width: parent.width + height: parent.height + + clip: visible + pressed: control.pressed + anchor: control + active: control.down || control.highlighted + color: control.MD.MatProp.stateLayerColor + } + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + property color leadingIconColor: item_state.leadingIconColor + property color trailingIconColor: item_state.trailingIconColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level2 + textColor: item_state.ctx.color.on_surface + backgroundColor: item_state.ctx.color.surface_container + supportTextColor: item_state.ctx.color.on_surface_variant + stateLayerColor: "transparent" + property color leadingIconColor: item_state.ctx.color.on_surface + property color trailingIconColor: item_state.ctx.color.on_surface + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.elevation: MD.Token.elevation.level0 + control.contentItem.opacity: 0.38 + } + }, + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.leadingIconColor: item_state.ctx.color.on_surface_variant + item_state.trailingIconColor: item_state.ctx.color.on_surface_variant + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.leadingIconColor: item_state.ctx.color.on_surface_variant + item_state.trailingIconColor: item_state.ctx.color.on_surface_variant + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/Page.qml b/qml/Page.qml new file mode 100644 index 0000000..7089828 --- /dev/null +++ b/qml/Page.qml @@ -0,0 +1,29 @@ +import QtQuick +import QtQuick.Templates as T +import Qcm.Material as MD + +T.Page { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding, + implicitHeaderWidth, + implicitFooterWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + MD.MatProp.elevation: MD.Token.elevation.level0 + + font.capitalization: Font.Capitalize + + background: Rectangle { + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.enabled && control.MD.MatProp.elevation > 0 + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + } +} \ No newline at end of file diff --git a/qml/Pane.qml b/qml/Pane.qml new file mode 100644 index 0000000..e9d9aaf --- /dev/null +++ b/qml/Pane.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtQuick.Templates as T +import Qcm.Material as MD + +T.Pane { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + padding: 12 + property real radius + MD.MatProp.elevation: MD.Token.elevation.level0 + + background: Rectangle { + color: control.MD.MatProp.backgroundColor + radius: control.radius + + layer.enabled: control.enabled && control.MD.MatProp.elevation > 0 + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + } +} \ No newline at end of file diff --git a/qml/Rail.qml b/qml/Rail.qml new file mode 100644 index 0000000..d8f1695 --- /dev/null +++ b/qml/Rail.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Button { + id: control + + property int iconStyle: hasIcon ? MD.Enum.IconAndText : MD.Enum.TextOnly + readonly property bool hasIcon: MD.Util.hasIcon(icon) + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + flat: false + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + + padding: 0 + spacing: 8 + + icon.width: 24 + icon.height: 24 + + font.weight: MD.Token.typescale.label_large.weight + font.pointSize: MD.Token.typescale.label_large.size + property int lineHeight: MD.Token.typescale.label_large.line_height + + contentItem: ColumnLayout { + spacing: 4 + MD.Control { + Layout.alignment: Qt.AlignHCenter + hoverEnabled: false + focusPolicy: Qt.NoFocus + leftInset: 12 + rightInset: 12 + + contentItem: MD.Icon { + name: control.icon.name + size: control.icon.width + color: control.MD.MatProp.supportTextColor + } + + background: Item { + implicitWidth: 56 + implicitHeight: 32 + Rectangle { + anchors.centerIn: parent + height: parent.height + width: 56 + + NumberAnimation on width { + alwaysRunToEnd: true + from: 48 + to: 56 + duration: 100 + running: control.checked + } + + radius: height / 2 + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.enabled && color.a > 0 && !control.flat + layer.effect: MD.RoundedElevationEffect { + elevation: MD.Token.elevation.level0//control.MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + } + } + } + + MD.Text { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + font.capitalization: Font.Capitalize + typescale: MD.Token.typescale.label_medium + text: control.text + prominent: control.checked + } + } + + background: Item { + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor // icon color + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level1 + textColor: control.checked ? item_state.ctx.color.on_surface : item_state.ctx.color.on_surface_variant + backgroundColor: control.checked ? item_state.ctx.color.secondary_container : "transparent" + supportTextColor: control.checked ? item_state.ctx.color.on_secondary_container : item_state.ctx.color.on_surface_variant + stateLayerColor: "transparent" + + states: [ + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.elevation: MD.Token.elevation.level1 + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: control.checked ? item_state.ctx.color.on_secondary_container : item_state.ctx.color.on_surface + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.elevation: MD.Token.elevation.level2 + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: control.checked ? item_state.ctx.color.on_secondary_container : item_state.ctx.color.on_surface + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/ScrollBar.qml b/qml/ScrollBar.qml new file mode 100644 index 0000000..b439c1b --- /dev/null +++ b/qml/ScrollBar.qml @@ -0,0 +1,71 @@ +import QtQuick +import QtQuick.Templates as T +import Qcm.Material as MD + +T.ScrollBar { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + padding: control.interactive ? 1 : 2 + visible: control.policy !== T.ScrollBar.AlwaysOff + minimumSize: orientation === Qt.Horizontal ? height / width : width / height + + interactive: hovered || pressed + + contentItem: Rectangle { + implicitWidth: control.interactive ? 6 : 2 + implicitHeight: control.interactive ? 6 : 2 + + radius: control.orientation === Qt.Horizontal ? height / 2.0 : width / 2.0 + + color: MD.Util.transparent(MD.Token.color.on_surface, control.pressed ? 0.8 : 0.38) + //control.pressed ? control.Material.scrollBarPressedColor : + // control.interactive && control.hovered ? control.Material.scrollBarHoveredColor : control.Material.scrollBarColor + opacity: 0.0 + } + + background: Rectangle { + implicitWidth: control.interactive ? 8 : 2 + implicitHeight: control.interactive ? 8 : 2 + color: MD.Util.transparent(MD.Token.color.on_surface, 0.12) + radius: control.orientation === Qt.Horizontal ? height / 2.0 : width / 2.0 + opacity: 0.0 + visible: control.interactive + } + + states: State { + name: "active" + when: control.policy === T.ScrollBar.AlwaysOn || (control.active && control.size < 1.0) + } + + transitions: [ + Transition { + to: "active" + NumberAnimation { + targets: [control.contentItem, control.background] + property: "opacity" + to: 1.0 + } + }, + Transition { + from: "active" + SequentialAnimation { + PropertyAction { + targets: [control.contentItem, control.background] + property: "opacity" + value: 1.0 + } + PauseAnimation { + duration: 2450 + } + NumberAnimation { + targets: [control.contentItem, control.background] + property: "opacity" + to: 0.0 + } + } + } + ] +} diff --git a/qml/SearchBar.qml b/qml/SearchBar.qml new file mode 100644 index 0000000..9843d36 --- /dev/null +++ b/qml/SearchBar.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Templates as T +import QtQuick.Controls as QC +import QtQuick.Controls.impl +import QtQuick.Controls.Material.impl as MDImpl +import Qcm.Material as MD + +T.Button { + id: control + + property bool leading_input: false + property QC.Action leading_action: QC.Action { + icon.name: MD.Token.icon.search + } + property bool trailing_input: control.text + property QC.Action trailing_action: QC.Action { + icon.name: control.text ? MD.Token.icon.close : null + onTriggered: { + item_input.text = ''; + } + } + signal accepted + + text: item_input.text + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + padding: 0 + leftPadding: 0 + rightPadding: item_trailing.visible ? 0 : 16 + + contentItem: RowLayout { + spacing: 0 + + MD.IconButton { + id: item_leading + action: control.leading_action + MD.InputBlock { + when: !control.leading_input + target: item_leading + } + } + + MD.TextFieldEmbed { + id: item_input + Layout.fillWidth: true + color: control.MD.MatProp.textColor + + Connections { + target: item_input + function onAccepted() { + control.accepted(); + } + } + } + + MD.IconButton { + id: item_trailing + action: control.trailing_action + visible: icon.name + MD.InputBlock { + when: !control.trailing_input + target: item_trailing + } + } + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 56 + + radius: height / 2 + color: control.MD.MatProp.backgroundColor + + layer.enabled: control.enabled && color.a > 0 + layer.effect: MD.RoundedElevationEffect { + elevation: control.MD.MatProp.elevation + } + + MD.Ripple { + clip: true + clipRadius: parent.radius + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level2 + textColor: item_state.ctx.color.on_surface + backgroundColor: item_state.ctx.color.surface_container_highest + supportTextColor: item_state.ctx.color.on_surface_variant + stateLayerColor: "transparent"//item_state.ctx.color.surface_tint + + property color outlineColor: item_state.ctx.color.outline + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.supportTextColor: item_state.ctx.color.on_surface + placeholder.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Pressed" + when: control.pressed || control.focus + PropertyChanges { + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/Slider.qml b/qml/Slider.qml new file mode 100644 index 0000000..e6c4080 --- /dev/null +++ b/qml/Slider.qml @@ -0,0 +1,137 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.Slider { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitHandleWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitHandleHeight + topPadding + bottomPadding) + + horizontalPadding: 0 + topInset: 0 + bottomInset: 0 + topPadding: 0 + bottomPadding: 0 + clip: false + + // The Slider is discrete if all of the following requirements are met: + // * stepSize is positive + // * snapMode is set to SnapAlways + // * the difference between to and from is cleanly divisible by the stepSize + // * the number of tick marks intended to be rendered is less than the width to height ratio, or vice versa for vertical sliders. + readonly property real __steps: Math.abs(to - from) / stepSize + readonly property bool __isDiscrete: stepSize >= Number.EPSILON && snapMode === Slider.SnapAlways && Math.abs(Math.round(__steps) - __steps) < Number.EPSILON && Math.floor(__steps) < (horizontal ? background.width / background.height : background.height / background.width) + + handle: MD.SliderHandle { + x: control.leftPadding + (control.horizontal ? control.visualPosition * (control.availableWidth - width) : (control.availableWidth - width) / 2) + y: control.topPadding + (control.horizontal ? (control.availableHeight - height) / 2 : control.visualPosition * (control.availableHeight - height)) + value: control.value + handleHasFocus: control.visualFocus + handlePressed: control.pressed + handleHovered: control.hovered + } + + background: Item { + x: control.leftPadding + (control.horizontal ? 0 : (control.availableWidth - width) / 2) + y: control.topPadding + (control.horizontal ? (control.availableHeight - height) / 2 : 0) + implicitWidth: control.horizontal ? 200 : 4 + implicitHeight: control.horizontal ? 4 : 200 + + Rectangle { + anchors.centerIn: parent + width: control.horizontal ? parent.width - (control.implicitHandleWidth - (control.__isDiscrete ? 4 : 0)) : 4 + height: control.horizontal ? 4 : parent.height - (control.implicitHandleHeight - (control.__isDiscrete ? 4 : 0)) + scale: control.horizontal && control.mirrored ? -1 : 1 + radius: Math.min(width, height) / 2 + color: control.trackInactiveColor + + Rectangle { + x: control.horizontal ? 0 : (parent.width - width) / 2 + y: control.horizontal ? (parent.height - height) / 2 : control.visualPosition * parent.height + width: control.horizontal ? control.position * parent.width : 4 + height: control.horizontal ? 4 : control.position * parent.height + radius: Math.min(width, height) / 2 + color: control.trackColor + } + + // Declaring this as a property (in combination with the parent binding below) avoids ids, + // which prevent deferred execution. + property Repeater repeater: Repeater { + parent: control.background.children[0] + model: control.__isDiscrete ? Math.floor(control.__steps) + 1 : 0 + delegate: Rectangle { + width: 2 + height: 2 + radius: 2 + x: control.horizontal ? (parent.width - width * 2) * currentPosition + (width / 2) : (parent.width - width) / 2 + y: control.horizontal ? (parent.height - height) / 2 : (parent.height - height * 2) * currentPosition + (height / 2) + color: active ? control.trackMarkColor : control.trackMarkInactiveColor + + required property int index + readonly property real currentPosition: index / (parent.repeater.count - 1) + readonly property bool active: (control.horizontal && control.visualPosition > currentPosition) || (!control.horizontal && control.visualPosition <= currentPosition) + } + } + } + } + property color trackColor: MD.MatProp.backgroundColor + property color trackInactiveColor: item_state.trackInactiveColor + property color trackMarkColor: MD.MatProp.supportTextColor + property color trackMarkInactiveColor: item_state.trackMarkInactiveColor + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level0 + textColor: item_state.ctx.color.on_primary + backgroundColor: item_state.ctx.color.primary + supportTextColor: item_state.ctx.color.on_primary + stateLayerColor: "#00000000" + + property color trackInactiveColor: item_state.ctx.color.surface_container_highest + property color trackMarkInactiveColor: item_state.ctx.color.on_surface_variant + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.backgroundColor: item_state.ctx.color.on_surface + item_state.trackInactiveColor: item_state.ctx.color.on_surface + + control.background.opacity: 0.38 + } + }, + State { + name: "Pressed" + when: control.pressed || control.focus + PropertyChanges { + item_state.stateLayerColor: { + const c = item_state.ctx.color.primary; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.stateLayerColor: { + const c = item_state.ctx.color.primary; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/SplitView.qml b/qml/SplitView.qml new file mode 100644 index 0000000..19bcde4 --- /dev/null +++ b/qml/SplitView.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.SplitView { + id: control + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + handle: Rectangle { + implicitWidth: control.orientation === Qt.Horizontal ? 6 : control.width + implicitHeight: control.orientation === Qt.Horizontal ? control.height : 6 + color: T.SplitHandle.pressed ? control.MD.MatProp.backgroundColor : Qt.lighter(control.MD.MatProp.backgroundColor, T.SplitHandle.hovered ? 1.1 : 1.0) + + Rectangle { + color: control.MD.MatProp.textColor + width: control.orientation === Qt.Horizontal ? thickness : length + height: control.orientation === Qt.Horizontal ? length : thickness + radius: thickness + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + property int length: parent.T.SplitHandle.pressed ? 3 : 8 + readonly property int thickness: parent.T.SplitHandle.pressed ? 3 : 1 + + Behavior on length { + NumberAnimation { + duration: 100 + } + } + } + } +} diff --git a/qml/StackView.qml b/qml/StackView.qml new file mode 100644 index 0000000..523a965 --- /dev/null +++ b/qml/StackView.qml @@ -0,0 +1,60 @@ + +import QtQuick +import QtQuick.Templates as T + +T.StackView { + id: control + + component LineAnimation: NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + + component FadeIn: LineAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + } + + component FadeOut: LineAnimation { + property: "opacity" + from: 1.0 + to: 0.0 + } + + popEnter: Transition { + // slide_in_left + LineAnimation { property: "x"; from: (control.mirrored ? -0.5 : 0.5) * -control.width; to: 0 } + FadeIn {} + } + + popExit: Transition { + // slide_out_right + LineAnimation { property: "x"; from: 0; to: (control.mirrored ? -0.5 : 0.5) * control.width } + FadeOut {} + } + + pushEnter: Transition { + // slide_in_right + LineAnimation { property: "x"; from: (control.mirrored ? -0.5 : 0.5) * control.width; to: 0 } + FadeIn {} + } + + pushExit: Transition { + // slide_out_left + LineAnimation { property: "x"; from: 0; to: (control.mirrored ? -0.5 : 0.5) * -control.width } + FadeOut {} + } + + replaceEnter: Transition { + // slide_in_right + LineAnimation { property: "x"; from: (control.mirrored ? -0.5 : 0.5) * control.width; to: 0 } + FadeIn {} + } + + replaceExit: Transition { + // slide_out_left + LineAnimation { property: "x"; from: 0; to: (control.mirrored ? -0.5 : 0.5) * -control.width } + FadeOut {} + } +} \ No newline at end of file diff --git a/qml/Switch.qml b/qml/Switch.qml new file mode 100644 index 0000000..cca82c8 --- /dev/null +++ b/qml/Switch.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl +import QtQuick.Templates as T +import Qcm.Material as MD + +T.Switch { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding) + + padding: 0 + spacing: 8 + topInset: 0 + bottomInset: 0 + rightInset: 0 + leftInset: 0 + + icon.width: 16 + icon.height: 16 + icon.color: item_state.textColor + + indicator: Rectangle { + id: indicator + width: 52 + height: 32 + radius: height / 2 + y: parent.height / 2 - height / 2 + color: item_state.backgroundColor + border.width: 2 + border.color: item_state.ctx.color.outline + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + Behavior on border.color { + ColorAnimation { + duration: 200 + } + } + + Rectangle { + id: handle + x: Math.max(offset, Math.min(parent.width - offset - width, control.visualPosition * parent.width - (width / 2))) + y: (parent.height - height) / 2 + // We use scale to allow us to enlarge the circle from the center, + // as using width/height will cause it to jump due to the position x/y bindings. + // However, a large enough scale on certain displays will show the triangles + // that make up the circle, so instead we make sure that the circle is always + // its largest size so that more triangles are used, and downscale instead. + width: normalSize * largestScale + height: normalSize * largestScale + radius: width / 2 + color: item_state.handleColor + scale: normalSize / largestSize + + readonly property int offset: 2 + readonly property real normalSize: item_state.handleSize + readonly property real largestSize: 28 + readonly property real largestScale: largestSize / normalSize + readonly property bool hasIcon: control.icon.name.length > 0 || control.icon.source.toString().length > 0 + + Behavior on x { + enabled: !control.pressed + SmoothedAnimation { + duration: 300 + } + } + + Behavior on scale { + NumberAnimation { + duration: 100 + } + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + + /* + IconImage { + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + name: control.icon.name + source: control.icon.source + sourceSize: Qt.size(control.icon.width, control.icon.height) + color: control.icon.color + visible: handle.hasIcon + } +*/ + + } + Ripple { + x: handle.x + handle.width / 2 - width / 2 + y: handle.y + handle.height / 2 - height / 2 + width: 28 + height: 28 + pressed: control.pressed + active: enabled && (control.down || control.visualFocus || control.hovered) + color: item_state.stateLayerColor + } + } + + contentItem: Item { + implicitWidth: control.indicator.width + implicitHeight: control.indicator.height + } + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level1 + textColor: control.checked ? item_state.ctx.color.on_primary_container : item_state.ctx.color.surface_container_highest + backgroundColor: control.checked ? item_state.ctx.color.primary : item_state.ctx.color.surface_container_highest + stateLayerColor: "transparent" + property color handleColor: control.checked ? item_state.ctx.color.on_primary : item_state.ctx.color.outline + property int handleSize: control.checked ? 24 : 16 + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.elevation: MD.Token.elevation.level0 + item_state.textColor: item_state.ctx.color.on_surface + item_state.backgroundColor: item_state.ctx.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.textColor: control.checked ? item_state.ctx.color.on_primary_container : item_state.ctx.color.surface_container_highest + item_state.backgroundColor: control.checked ? item_state.ctx.color.primary : item_state.ctx.color.surface_container_highest + item_state.handleColor: control.checked ? item_state.ctx.color.primary_container : item_state.ctx.color.on_surface_variant + item_state.handleSize: 28 + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = control.checked ? item_state.ctx.color.primary : item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.textColor: control.checked ? item_state.ctx.color.on_primary_container : item_state.ctx.color.surface_container_highest + item_state.backgroundColor: control.checked ? item_state.ctx.color.primary : item_state.ctx.color.surface_container_highest + item_state.handleColor: control.checked ? item_state.ctx.color.primary_container : item_state.ctx.color.on_surface_variant + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = control.checked ? item_state.ctx.color.primary : item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/TabBar.qml b/qml/TabBar.qml new file mode 100644 index 0000000..6916dc8 --- /dev/null +++ b/qml/TabBar.qml @@ -0,0 +1,56 @@ + +import QtQuick +import QtQuick.Templates as T +import Qcm.Material as MD + +T.TabBar { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + spacing: 1 + + contentItem: ListView { + model: control.contentModel + currentIndex: control.currentIndex + + spacing: control.spacing + orientation: ListView.Horizontal + flickableDirection: Flickable.AutoFlickIfNeeded + snapMode: ListView.SnapToItem + + highlightMoveDuration: 250 + highlightResizeDuration: 0 + highlightFollowsCurrentItem: true + highlightRangeMode: ListView.ApplyRange + preferredHighlightBegin: 48 + preferredHighlightEnd: width - 48 + + highlight: Item { + z: 2 + Rectangle { + height: 2 + width: parent.width + y: control.position === T.TabBar.Footer ? 0 : parent.height - height + color: MD.Token.color.primary + } + } + } + + background: Rectangle { + color: MD.Token.color.surface + + //layer.enabled: control.Material.elevation > 0 + //layer.effect: MD.ElevationEffect { + // elevation: control.Material.elevation + // fullWidth: true + //} + + MD.Divider { + anchors.bottom: parent.bottom + } + } +} \ No newline at end of file diff --git a/qml/TabButton.qml b/qml/TabButton.qml new file mode 100644 index 0000000..de75a78 --- /dev/null +++ b/qml/TabButton.qml @@ -0,0 +1,102 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qcm.Material as MD + +T.TabButton { + id: control + + property int iconStyle: hasIcon ? MD.Enum.IconAndText : MD.Enum.TextOnly + readonly property bool hasIcon: MD.Util.hasIcon(icon) + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding) + + topInset: 0 + bottomInset: 0 + leftInset: 0 + rightInset: 0 + spacing: 8 + + leftPadding: 12 + rightPadding: 12 + + icon.width: 24 + icon.height: 24 + + font.weight: MD.Token.typescale.title_small.weight + font.pixelSize: MD.Token.typescale.title_small.size + property int lineHeight: MD.Token.typescale.title_small.line_height + + contentItem: MD.IconLabel { + lineHeight: control.lineHeight + + font: control.font + text: control.text + icon_style: control.iconStyle + + icon_name: control.icon.name + icon_size: control.icon.width + } + + background: MD.Ripple { + implicitHeight: 48 + + clip: true + pressed: control.pressed + anchor: control + active: enabled && (control.down || control.visualFocus || control.hovered) + color: control.MD.MatProp.stateLayerColor + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level0 + textColor: control.checked ? item_state.ctx.color.on_surface : item_state.ctx.color.on_surface_variant + backgroundColor: item_state.ctx.color.surface + stateLayerColor: "transparent" + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.backgroundColor: item_state.ctx.color.on_surface + control.contentItem.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "Pressed" + when: control.down || control.focus + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.stateLayerColor: { + const c = item_state.ctx.color.on_surface; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/Text.qml b/qml/Text.qml new file mode 100644 index 0000000..418208a --- /dev/null +++ b/qml/Text.qml @@ -0,0 +1,24 @@ +import QtQuick +import Qcm.Material as MD + +Text { + id: root + property MD.t_typescale typescale: MD.Token.typescale.label_medium + property bool prominent: false + + Binding { + when: typescale + root.lineHeight: typescale ? typescale?.line_height : 16 + root.font.pixelSize: typescale ? typescale?.size : 16 + root.font.weight: typescale ? (root.prominent ? typescale.weight_prominent : typescale.weight) : Font.Normal + root.font.letterSpacing: typescale ? typescale.tracking : 1 + restoreMode: Binding.RestoreNone + } + + color: MD.MatProp.textColor + lineHeightMode: Text.FixedHeight + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 1 + textFormat: Text.PlainText +} diff --git a/qml/TextField.qml b/qml/TextField.qml new file mode 100644 index 0000000..8e8d5ee --- /dev/null +++ b/qml/TextField.qml @@ -0,0 +1,175 @@ +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import QtQuick.Controls.Material.impl as MDImpl +import Qcm.Material as MD + +MD.TextFieldEmbed { + id: control + + property int type: MD.Enum.TextFieldOutlined + property string leading_icon + property string trailing_icon + + font.capitalization: Font.MixedCase + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding + (leading.visible ? leading.implicitWidth : 0) + (trailing.visible ? trailing.implicitWidth : 0)) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + // If we're clipped, set topInset to half the height of the placeholder text to avoid it being clipped. + topInset: clip ? placeholder.largestHeight / 2 : 0 + bottomInset: 0 + + leftPadding: 16 + rightPadding: 16 + topPadding: 0 + bottomPadding: 0 + + // Need to account for the placeholder text when it's sitting on top. + //topPadding: Material.containerStyle === Material.Filled + // ? placeholderText.length > 0 && (activeFocus || length > 0) + // ? Material.textFieldVerticalPadding + placeholder.largestHeight + // : Material.textFieldVerticalPadding + // // Account for any topInset (used to avoid floating placeholder text being clipped), + // // otherwise the text will be too close to the background. + // : Material.textFieldVerticalPadding + topInset + + MDImpl.FloatingPlaceholderText { + id: placeholder + x: control.leftPadding + width: control.width - (control.leftPadding + control.rightPadding) + text: control.placeholderText + font: control.font + color: control.placeholderTextColor + elide: Text.ElideRight + renderType: control.renderType + + filled: control.type === MD.Enum.TextFieldFilled + verticalPadding: 8 + controlHasActiveFocus: control.activeFocus + controlHasText: control.length > 0 + controlImplicitBackgroundHeight: control.implicitBackgroundHeight + controlHeight: control.height + } + + property Item leading: MD.Icon { + anchors.left: parent?.left + anchors.verticalCenter: parent?.verticalCenter + anchors.leftMargin: 12 + name: control.leading_icon + visible: name + size: 24 + } + + property Item trailing: MD.Icon { + anchors.right: parent?.right + anchors.verticalCenter: parent?.verticalCenter + anchors.rightMargin: 12 + visible: name + name: control.trailing_icon + size: 24 + } + + data: [placeholder, leading, trailing] + + background: MDImpl.MaterialTextContainer { + implicitWidth: 64 + implicitHeight: 56 + + filled: control.type === MD.Enum.TextFieldFilled + fillColor: control.MD.MatProp.backgroundColor + outlineColor: control.outlineColor + focusedOutlineColor: control.outlineColor + placeholderTextWidth: Math.min(placeholder.width, placeholder.implicitWidth) * placeholder.scale + controlHasActiveFocus: control.activeFocus + controlHasText: control.length > 0 + placeholderHasText: placeholder.text.length > 0 + horizontalPadding: 16 + } + + MD.MatProp.elevation: item_state.elevation + MD.MatProp.textColor: item_state.textColor + MD.MatProp.supportTextColor: item_state.supportTextColor + MD.MatProp.backgroundColor: item_state.backgroundColor + MD.MatProp.stateLayerColor: item_state.stateLayerColor + + color: item_state.textColor + placeholderTextColor: item_state.placeholderColor + + property color outlineColor: item_state.outlineColor + + MD.State { + id: item_state + item: control + + elevation: MD.Token.elevation.level0 + textColor: item_state.ctx.color.on_surface + backgroundColor: "transparent" + supportTextColor: item_state.ctx.color.on_surface_variant + property color placeholderColor: item_state.ctx.color.on_surface_variant + property color outlineColor: item_state.ctx.color.outline + + states: [ + State { + name: "Disabled" + when: !enabled + PropertyChanges { + item_state.placeholderColor: item_state.ctx.color.on_surface + item_state.supportTextColor: item_state.ctx.color.on_surface + placeholder.opacity: 0.38 + control.background.opacity: 0.12 + } + }, + State { + name: "ErrorHover" + when: !control.acceptableInput && control.hovered + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: item_state.ctx.color.error + item_state.placeholderColor: item_state.ctx.color.on_error_container + item_state.outlineColor: item_state.ctx.color.on_error_container + } + }, + State { + name: "Error" + when: !control.acceptableInput + PropertyChanges { + item_state.textColor: item_state.ctx.color.on_surface + item_state.supportTextColor: item_state.ctx.color.error + item_state.placeholderColor: item_state.ctx.color.error + item_state.outlineColor: item_state.ctx.color.error + } + }, + State { + name: "Focused" + when: control.focus + PropertyChanges { + item_state.placeholderColor: item_state.ctx.color.primary + item_state.outlineColor: item_state.ctx.color.primary + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.primary; + return MD.Util.transparent(c, MD.Token.state.pressed.state_layer_opacity); + } + } + }, + State { + name: "Hovered" + when: control.hovered + PropertyChanges { + item_state.placeholderColor: item_state.ctx.color.on_surface + item_state.outlineColor: item_state.ctx.color.on_surface + } + PropertyChanges { + restoreEntryValues: false + item_state.stateLayerColor: { + const c = item_state.ctx.color.primary; + return MD.Util.transparent(c, MD.Token.state.hover.state_layer_opacity); + } + } + } + ] + } +} diff --git a/qml/TextInput.qml b/qml/TextInput.qml new file mode 100644 index 0000000..3c6da2a --- /dev/null +++ b/qml/TextInput.qml @@ -0,0 +1,27 @@ +import QtQuick +import Qcm.Material as MD + +TextInput { + id: root + property MD.t_typescale typescale: MD.Token.typescale.body_large + property bool prominent: false + + font.capitalization: Font.MixedCase + + Binding { + when: typescale + root.font.pixelSize: typescale.size + root.font.weight: root.prominent && typescale.weight_prominent ? typescale.weight_prominent : typescale.weight + root.font.letterSpacing: typescale.tracking + restoreMode: Binding.RestoreNone + } + + cursorDelegate: MD.CursorDelegate { + } + + color: MD.MatProp.textColor + selectionColor: MD.Token.color.primary + selectedTextColor: MD.Token.color.getOn(selectionColor) + + verticalAlignment: TextInput.AlignVCenter +} diff --git a/qml/Token.qml b/qml/Token.qml new file mode 100644 index 0000000..2ff2c3f --- /dev/null +++ b/qml/Token.qml @@ -0,0 +1,95 @@ +pragma Singleton +import QtCore +import QtQml +import QtQuick +import Qcm.Material as MD + +Item { + id: root + readonly property QtObject color: root.MD.MatProp.color + property int theme: root.color.colorScheme + readonly property var icon: MD.IconToken.codeMap + readonly property bool is_dark_theme: Number(theme) == MD.MdColorMgr.Dark + + // typescale + readonly property QtObject font: QtObject { + readonly property var default_font: Qt.application.font + readonly property FontLoader fontload_material_outline: FontLoader { + source: 'qrc:/Qcm/Material/assets/MaterialIconsOutlined-Regular.otf' + } + readonly property FontLoader fontload_material_round: FontLoader { + source: 'qrc:/Qcm/Material/assets/MaterialIconsRound-Regular.otf' + } + readonly property var icon_outline: this.fontload_material_outline.font + readonly property var icon_round: this.fontload_material_round.font + } + + + // Font.Thin 0 + // Font.ExtraLight 12 + // Font.Light 25 + // Font.Normal 50 + // Font.Medium 57 + // Font.DemiBold 63 + // Font.Bold 75 + // Font.ExtraBold 81 + // Font.Black 87 + + // Value Common weight name + // 100 Thin (Hairline) + // 200 Extra Light (Ultra Light) + // 300 Light + // 400 Normal (Regular) + // 500 Medium + // 600 Semi Bold (Demi Bold) + // 700 Bold + // 800 Extra Bold (Ultra Bold) + // 900 Black (Heavy) + // 950 Extra Black (Ultra Black) + + readonly property QtObject typescale: MD.TypeScale { + } + + readonly property QtObject state: QtObject { + readonly property QtObject hover: QtObject { + readonly property real state_layer_opacity: 0.08 + } + readonly property QtObject pressed: QtObject { + readonly property real state_layer_opacity: 0.1 + } + readonly property QtObject focus: QtObject { + readonly property real state_layer_opacity: 0.1 + } + } + readonly property QtObject elevation: QtObject { + readonly property int level0: 0 + readonly property int level1: 1 + readonly property int level2: 3 + readonly property int level3: 6 + readonly property int level4: 8 + readonly property int level5: 12 + } + + readonly property QtObject shape: QtObject { + readonly property QtObject corner: QtObject { + readonly property int extra_large: 28 + readonly property int extra_small: 4 + } + } + + // seems icon font size need map + function ic_size(s) { + switch (Math.floor(s)) { + case 24: + return 18; + default: + return s; + } + } + function toMatTheme(th, inverse = false) { + function fn_inverse(v, iv) { + return iv ? !v : v; + } + return fn_inverse(th === MdColorMgr.Dark, inverse) ? Material.Dark : Material.Light; + } +} diff --git a/qml/impl/BoxShadow.qml b/qml/impl/BoxShadow.qml new file mode 100644 index 0000000..8d301f3 --- /dev/null +++ b/qml/impl/BoxShadow.qml @@ -0,0 +1,31 @@ +import QtQuick +import Qcm.Material as MD + +MD.RectangularGlow { + property int offsetX + property int offsetY + property int blurRadius + property int spreadRadius + + property real strength + + property Item source + + property bool fullWidth + property bool fullHeight + + readonly property real sourceRadius: source && source.radius || 0 + + x: (parent.width - width)/2 + offsetX + y: (parent.height - height)/2 + offsetY + + implicitWidth: source ? source.width : parent.width + implicitHeight: source ? source.height : parent.height + + width: implicitWidth + 2 * spreadRadius + (fullWidth ? 2 * cornerRadius : 0) + height: implicitHeight + 2 * spreadRadius + (fullHeight ? 2 * cornerRadius : 0) + glowRadius: blurRadius/2 + spread: strength + + cornerRadius: blurRadius + sourceRadius +} \ No newline at end of file diff --git a/qml/impl/CursorDelegate.qml b/qml/impl/CursorDelegate.qml new file mode 100644 index 0000000..22d2ab8 --- /dev/null +++ b/qml/impl/CursorDelegate.qml @@ -0,0 +1,31 @@ + +import QtQuick + +import Qcm.Material as MD + +Rectangle { + id: cursor + + color: MD.Token.color.primary + width: 2 + visible: parent.activeFocus && !parent.readOnly && parent.selectionStart === parent.selectionEnd + + Connections { + target: cursor.parent + function onCursorPositionChanged() { + // keep a moving cursor visible + cursor.opacity = 1 + timer.restart() + } + } + + Timer { + id: timer + running: cursor.parent.activeFocus && !cursor.parent.readOnly && interval != 0 + repeat: true + interval: Qt.styleHints.cursorFlashTime / 2 + onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0 + // force the cursor visible when gaining focus + onRunningChanged: cursor.opacity = 1 + } +} \ No newline at end of file diff --git a/qml/impl/ElevationEffect.qml b/qml/impl/ElevationEffect.qml new file mode 100644 index 0000000..ca94f03 --- /dev/null +++ b/qml/impl/ElevationEffect.qml @@ -0,0 +1,192 @@ +import QtQuick +import Qcm.Material as MD + +// https://github.com/material-components/material-web/blob/v1.2.0/elevation/internal/_elevation.scss + +Item { + id: effect + + property var source + + property int elevation: 0 + + property bool fullWidth: false + property bool fullHeight: false + + readonly property Item sourceItem: source.sourceItem + + property var _shadows: _defaultShadows + + readonly property var _defaultShadows: [ + { // 0 + angularValues: [ + {offset: 0, blur: 0, spread: 0}, + {offset: 0, blur: 0, spread: 0}, + {offset: 0, blur: 0, spread: 0} + ], + strength: 0.05 + }, + { // 1 + angularValues: [ + {offset: 1, blur: 3, spread: 0}, + {offset: 1, blur: 1, spread: 0}, + {offset: 2, blur: 1, spread: -1} + ], + strength: 0.05 + }, + { // 2 + angularValues: [ + {offset: 1, blur: 5, spread: 0}, + {offset: 2, blur: 2, spread: 0}, + {offset: 3, blur: 1, spread: -2} + ], + strength: 0.05 + }, + { // 3 + angularValues: [ + {offset: 1, blur: 8, spread: 0}, + {offset: 3, blur: 4, spread: 0}, + {offset: 3, blur: 3, spread: -2} + ], + strength: 0.05 + }, + { // 4 + angularValues: [ + {offset: 2, blur: 4, spread: -1}, + {offset: 4, blur: 5, spread: 0}, + {offset: 1, blur: 10, spread: 0} + ], + strength: 0.05 + }, + { // 5 + angularValues: [ + {offset: 3, blur: 5, spread: -1}, + {offset: 5, blur: 8, spread: 0}, + {offset: 1, blur: 14, spread: 0} + ], + strength: 0.05 + }, + { // 6 + angularValues: [ + {offset: 3, blur: 5, spread: -1}, + {offset: 6, blur: 10, spread: 0}, + {offset: 1, blur: 18, spread: 0} + ], + strength: 0.05 + }, + { // 7 + angularValues: [ + {offset: 4, blur: 5, spread: -2}, + {offset: 7, blur: 10, spread: 1}, + {offset: 2, blur: 16, spread: 1} + ], + strength: 0.05 + }, + { // 8 + angularValues: [ + {offset: 5, blur: 5, spread: -3}, + {offset: 8, blur: 10, spread: 1}, + {offset: 3, blur: 14, spread: 2} + ], + strength: 0.05 + }, + { // 9 + angularValues: [ + {offset: 5, blur: 6, spread: -3}, + {offset: 9, blur: 12, spread: 1}, + {offset: 3, blur: 16, spread: 2} + ], + strength: 0.05 + }, + { // 10 + angularValues: [ + {offset: 6, blur: 6, spread: -3}, + {offset: 10, blur: 14, spread: 1}, + {offset: 4, blur: 18, spread: 3} + ], + strength: 0.05 + }, + { // 11 + angularValues: [ + {offset: 6, blur: 7, spread: -4}, + {offset: 11, blur: 15, spread: 1}, + {offset: 4, blur: 20, spread: 3} + ], + strength: 0.05 + }, + { // 12 + angularValues: [ + {offset: 7, blur: 8, spread: -4}, + {offset: 12, blur: 17, spread: 2}, + {offset: 5, blur: 22, spread: 4} + ], + strength: 0.05 + } + ] + + readonly property var _shadow: _shadows[Math.max(0, Math.min(elevation, _shadows.length - 1))] + + Item { + property int margin: -100 + + x: margin + y: margin + width: parent.width - 2 * margin + height: parent.height - 2 * margin + + // By rendering as a layer, the shadow will never show through the source item, + // even when the source item's opacity is less than 1 + layer.enabled: true + + // The box shadows automatically pick up the size of the source Item and not + // the size of the parent, so we don't need to worry about the extra padding + // in the parent Item + + // key box + MD.BoxShadow { + offsetY: effect._shadow.angularValues[0].offset + blurRadius: effect._shadow.angularValues[0].blur + spreadRadius: effect._shadow.angularValues[0].spread + strength: effect._shadow.strength + color: Qt.rgba(0,0,0, 0.2) + + fullWidth: effect.fullWidth + fullHeight: effect.fullHeight + source: effect.sourceItem + } + + // ambient box + MD.BoxShadow { + offsetY: effect._shadow.angularValues[1].offset + blurRadius: effect._shadow.angularValues[1].blur + spreadRadius: effect._shadow.angularValues[1].spread + strength: effect._shadow.strength + color: Qt.rgba(0,0,0, 0.14) + + fullWidth: effect.fullWidth + fullHeight: effect.fullHeight + source: effect.sourceItem + } + + MD.BoxShadow { + offsetY: effect._shadow.angularValues[2].offset + blurRadius: effect._shadow.angularValues[2].blur + spreadRadius: effect._shadow.angularValues[2].spread + strength: effect._shadow.strength + color: Qt.rgba(0,0,0, 0.12) + + fullWidth: effect.fullWidth + fullHeight: effect.fullHeight + source: effect.sourceItem + } + + ShaderEffect { + property alias source: effect.source + + x: (parent.width - width)/2 + y: (parent.height - height)/2 + width: effect.sourceItem.width + height: effect.sourceItem.height + } + } +} \ No newline at end of file diff --git a/qml/impl/ListBusyFooter.qml b/qml/impl/ListBusyFooter.qml new file mode 100644 index 0000000..c8a17e2 --- /dev/null +++ b/qml/impl/ListBusyFooter.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Controls +import Qcm.Material as MD + +MD.Pane { + property alias running: m_busy.running + + visible: running + implicitHeight: m_busy.running ? m_busy.implicitHeight + 2 * padding : 0 + padding: 4 + clip: true + + MD.CircularIndicator { + id: m_busy + + anchors.centerIn: parent + } +} diff --git a/qml/impl/RectangularGlow.qml b/qml/impl/RectangularGlow.qml new file mode 100644 index 0000000..d032d6e --- /dev/null +++ b/qml/impl/RectangularGlow.qml @@ -0,0 +1,204 @@ +import QtQuick + +/* + A cross-graphics API implementation of QtGraphicalEffects' RectangularGlow. + */ +Item { + id: rootItem + + /* + This property defines how many pixels outside the item area are reached + by the glow. + + The value ranges from 0.0 (no glow) to inf (infinite glow). By default, + the property is set to \c 0.0. + + \table + \header + \li Output examples with different glowRadius values + \li + \li + \row + \li \image RectangularGlow_glowRadius1.png + \li \image RectangularGlow_glowRadius2.png + \li \image RectangularGlow_glowRadius3.png + \row + \li \b { glowRadius: 10 } + \li \b { glowRadius: 20 } + \li \b { glowRadius: 40 } + \row + \li \l spread: 0 + \li \l spread: 0 + \li \l spread: 0 + \row + \li \l color: #ffffff + \li \l color: #ffffff + \li \l color: #ffffff + \row + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \endtable + + */ + property real glowRadius: 0.0 + + /* + This property defines how large part of the glow color is strenghtened + near the source edges. + + The value ranges from 0.0 (no strenght increase) to 1.0 (maximum + strenght increase). By default, the property is set to \c 0.0. + + \table + \header + \li Output examples with different spread values + \li + \li + \row + \li \image RectangularGlow_spread1.png + \li \image RectangularGlow_spread2.png + \li \image RectangularGlow_spread3.png + \row + \li \b { spread: 0.0 } + \li \b { spread: 0.5 } + \li \b { spread: 1.0 } + \row + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \row + \li \l color: #ffffff + \li \l color: #ffffff + \li \l color: #ffffff + \row + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \endtable + */ + property real spread: 0.0 + + /* + This property defines the RGBA color value which is used for the glow. + + By default, the property is set to \c "white". + + \table + \header + \li Output examples with different color values + \li + \li + \row + \li \image RectangularGlow_color1.png + \li \image RectangularGlow_color2.png + \li \image RectangularGlow_color3.png + \row + \li \b { color: #ffffff } + \li \b { color: #55ff55 } + \li \b { color: #5555ff } + \row + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \row + \li \l spread: 0 + \li \l spread: 0 + \li \l spread: 0 + \row + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \endtable + */ + property color color: "white" + + /* + This property defines the corner radius that is used to draw a glow with + rounded corners. + + The value ranges from 0.0 to half of the effective width or height of + the glow, whichever is smaller. This can be calculated with: \c{ + min(width, height) / 2.0 + glowRadius} + + By default, the property is bound to glowRadius property. The glow + behaves as if the rectangle was blurred when adjusting the glowRadius + property. + + \table + \header + \li Output examples with different cornerRadius values + \li + \li + \row + \li \image RectangularGlow_cornerRadius1.png + \li \image RectangularGlow_cornerRadius2.png + \li \image RectangularGlow_cornerRadius3.png + \row + \li \b { cornerRadius: 0 } + \li \b { cornerRadius: 25 } + \li \b { cornerRadius: 50 } + \row + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \row + \li \l spread: 0 + \li \l spread: 0 + \li \l spread: 0 + \row + \li \l color: #ffffff + \li \l color: #ffffff + \li \l color: #ffffff + \endtable + */ + property real cornerRadius: glowRadius + + /* + This property allows the effect output pixels to be cached in order to + improve the rendering performance. + + Every time the source or effect properties are changed, the pixels in + the cache must be updated. Memory consumption is increased, because an + extra buffer of memory is required for storing the effect output. + + It is recommended to disable the cache when the source or the effect + properties are animated. + + By default, the property is set to \c false. + */ + property bool cached: false + + ShaderEffectSource { + id: cacheItem + anchors.fill: shaderItem + visible: rootItem.cached + smooth: true + sourceItem: shaderItem + live: true + hideSource: visible + } + + ShaderEffect { + id: shaderItem + + x: (parent.width - width) / 2.0 + y: (parent.height - height) / 2.0 + width: parent.width + rootItem.glowRadius * 2 + cornerRadius * 2 + height: parent.height + rootItem.glowRadius * 2 + cornerRadius * 2 + + function clampedCornerRadius() { + var maxCornerRadius = Math.min(rootItem.width, rootItem.height) / 2 + rootItem.glowRadius; + return Math.max(0, Math.min(rootItem.cornerRadius, maxCornerRadius)) + } + + property color color: rootItem.color + property real inverseSpread: 1.0 - rootItem.spread + property real relativeSizeX: ((inverseSpread * inverseSpread) * rootItem.glowRadius + cornerRadius * 2.0) / width + property real relativeSizeY: relativeSizeX * (width / height) + property real spread: rootItem.spread / 2.0 + property real cornerRadius: clampedCornerRadius() + + fragmentShader: 'qrc:/Qcm/Material/assets/shader/rect_glow.frag.qsb' + } +} \ No newline at end of file diff --git a/qml/impl/Ripple.qml b/qml/impl/Ripple.qml new file mode 100644 index 0000000..18e8a27 --- /dev/null +++ b/qml/impl/Ripple.qml @@ -0,0 +1,4 @@ +import QtQuick.Controls.Material.impl as MDImpl + +MDImpl.Ripple { +} \ No newline at end of file diff --git a/qml/impl/RoundClip.qml b/qml/impl/RoundClip.qml new file mode 100644 index 0000000..b0fd2ed --- /dev/null +++ b/qml/impl/RoundClip.qml @@ -0,0 +1,11 @@ +import QtQuick +import Qcm.Material as MD + +ShaderEffect { + property var source + property var radius: 0 + property vector4d radius_: MD.Util.corner(radius).toVector4D() + property real size: 1 + property real smoothing: 0.8 + fragmentShader: 'qrc:/Qcm/Material/assets/shader/round.frag.qsb' +} diff --git a/qml/impl/RoundedElevationEffect.qml b/qml/impl/RoundedElevationEffect.qml new file mode 100644 index 0000000..d335f5a --- /dev/null +++ b/qml/impl/RoundedElevationEffect.qml @@ -0,0 +1,33 @@ +import Qcm.Material as MD + +import QtQuick.Controls.Material as MT + +MD.ElevationEffect { + property int roundedScale: MT.Material.FullScale + + _shadows: roundedScale === MT.Material.NotRounded ? _defaultShadows : roundedShadows() + + function roundedShadows() { + // Make a deep copy. + let shadows = [..._defaultShadows] + for (let i = 0, strength = 0.95; i < shadows.length; ++i) { + // See comment on BoxShadow's strength property for why we do this. + shadows[i].strength = strength + // We don't want the strength to be too high for the controls with very slightly rounded + // corners, as they are quite close to the non-rounded ones in terms of not needing adjustments. + // This is still not great for the higher elevations for ExtraSmallScale, but it's as good + // as I can get it. + strength = Math.max(0.05, strength - (roundedScale > MT.Material.ExtraSmallScale ? 0.1 : 0.3)) + + // The values at index 0 are already 0, and we don't want our Math.max(1, ...) code to affect them. + if (i > 0) { + // The blur values for e.g. buttons with rounded corners are too large, so we reduce them. + for (let angularShadowIndex = 0; angularShadowIndex < shadows[i].angularValues.length; ++angularShadowIndex) { + shadows[i].angularValues[angularShadowIndex].blur = + Math.max(1, Math.floor(shadows[i].angularValues[angularShadowIndex].blur / 4)) + } + } + } + return shadows + } +} \ No newline at end of file diff --git a/qml/impl/Shadow.qml b/qml/impl/Shadow.qml new file mode 100644 index 0000000..c62df2c --- /dev/null +++ b/qml/impl/Shadow.qml @@ -0,0 +1,28 @@ +import QtQuick + +Item { + id: root + + property vector2d lower + property vector2d higher + property real sigma + property real corner + property color color: "white" + + ShaderEffect { + id: shaderItem + + x: (parent.width - width) / 2.0 + y: (parent.height - height) / 2.0 + width: parent.width + height: parent.height + + property vector2d lower: root.lower + property vector2d higher: root.higher + property real sigma: root.sigma + property real corner: root.corner + property color color: root.color + + fragmentShader: 'qrc:/Qcm/Material/assets/shader/shadow.frag.qsb' + } +} diff --git a/qml/impl/SliderHandle.qml b/qml/impl/SliderHandle.qml new file mode 100644 index 0000000..27227eb --- /dev/null +++ b/qml/impl/SliderHandle.qml @@ -0,0 +1,38 @@ +import QtQuick +import Qcm.Material as MD + +Item { + id: root + implicitWidth: 20 + implicitHeight: 20 + + property real value: 0 + property bool handleHasFocus: false + property bool handlePressed: false + property bool handleHovered: false + readonly property int initialSize: 4 + readonly property var control: parent + + Rectangle { + id: handleRect + anchors.centerIn: parent + width: 20; height: 20 + radius: width / 2 + color: root.control + ? root.control.MD.MatProp.backgroundColor + : "transparent" + + layer.enabled: true + layer.effect: MD.RoundedElevationEffect { + elevation: MD.Token.elevation.level1 + } + } + + MD.Ripple { + anchors.centerIn: parent + width: 28; height: 28 + pressed: root.handlePressed + active: root.handlePressed || root.handleHasFocus || (enabled && root.handleHovered) + color: root.control ? MD.Util.transparent(root.control.MD.MatProp.backgroundColor, MD.Token.state.hover.state_layer_opacity) : "transparent" + } +} \ No newline at end of file diff --git a/qml/impl/State.qml b/qml/impl/State.qml new file mode 100644 index 0000000..2fe4ea2 --- /dev/null +++ b/qml/impl/State.qml @@ -0,0 +1,16 @@ +import QtQuick +import Qcm.Material as MD + +Item { + id: root + visible: false + + property QtObject item + property var ctx: item?.MD.MatProp + + property int elevation + property color textColor + property color backgroundColor + property color supportTextColor + property color stateLayerColor +} \ No newline at end of file diff --git a/qml/impl/TextFieldEmbed.qml b/qml/impl/TextFieldEmbed.qml new file mode 100644 index 0000000..c237de2 --- /dev/null +++ b/qml/impl/TextFieldEmbed.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Templates as T +import Qcm.Material as MD + +T.TextField { + id: root + property MD.t_typescale typescale: MD.Token.typescale.body_large + property bool prominent: false + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + font.capitalization: Font.MixedCase + + Binding { + when: typescale + root.font.pixelSize: typescale.size + root.font.weight: root.prominent && typescale.weight_prominent ? typescale.weight_prominent : typescale.weight + root.font.letterSpacing: typescale.tracking + restoreMode: Binding.RestoreNone + } + + cursorDelegate: MD.CursorDelegate { + } + selectionColor: MD.Token.color.primary + selectedTextColor: MD.Token.color.getOn(selectionColor) + + verticalAlignment: TextInput.AlignVCenter + color: MD.MatProp.textColor +} diff --git a/qml/js/util.mjs b/qml/js/util.mjs new file mode 100644 index 0000000..e69de29 diff --git a/src/color.cpp b/src/color.cpp new file mode 100644 index 0000000..c93baec --- /dev/null +++ b/src/color.cpp @@ -0,0 +1,130 @@ +#include "qml_material/color.h" + +#include +#include + +#include "core/log.h" +#include "qml_material/helper.h" +#include "qml_material/util.h" + +using namespace qcm; +using namespace qml_material; + +namespace +{ +constexpr QRgb BASE_COLOR { qRgb(190, 231, 253) }; + +std::map gen_on_map(const MdScheme& sh) { + return { + { sh.primary, sh.on_primary }, + { sh.primary_container, sh.on_primary_container }, + { sh.secondary, sh.on_secondary }, + { sh.secondary_container, sh.on_secondary_container }, + { sh.tertiary, sh.on_tertiary }, + { sh.tertiary_container, sh.on_tertiary_container }, + { sh.error, sh.on_error }, + { sh.background, sh.on_background }, + { sh.inverse_surface, sh.inverse_on_surface }, + { sh.surface, sh.on_surface }, + { sh.surface_variant, sh.on_surface_variant }, + { sh.surface_1, sh.on_surface }, + { sh.surface_2, sh.on_surface }, + { sh.surface_3, sh.on_surface }, + { sh.surface_4, sh.on_surface }, + { sh.surface_5, sh.on_surface }, + { sh.surface_dim, sh.on_surface }, + { sh.surface_bright, sh.on_surface }, + { sh.surface_container, sh.on_surface }, + { sh.surface_container_high, sh.on_surface }, + { sh.surface_container_highest, sh.on_surface }, + { sh.surface_container_low, sh.on_surface }, + { sh.surface_container_lowest, sh.on_surface }, + }; +} +} // namespace + +DEFINE_CONVERT(qml_material::MdColorMgr::ColorSchemeEnum, Qt::ColorScheme) { + switch (in) { + case in_type::Dark: out = out_type::Dark; break; + default: out = out_type::Light; + } +} + +MdColorMgr::MdColorMgr(QObject* parent) + : QObject(parent), + m_accent_color(BASE_COLOR), + m_color_scheme(sysColorScheme()), + m_use_sys_color_scheme(true), + m_use_sys_accent_color(false) { + gen_scheme(); + connect(this, &Self::colorSchemeChanged, this, &Self::gen_scheme); + connect(this, &Self::accentColorChanged, this, &Self::gen_scheme); + + connect(Xdp::insance(), &Xdp::colorSchemeChanged, this, &Self::refrehFromSystem); + connect(Xdp::insance(), &Xdp::accentColorChanged, this, &Self::refrehFromSystem); + connect(this, &Self::useSysColorSMChanged, this, &Self::refrehFromSystem); + connect(this, &Self::useSysAccentColorChanged, this, &Self::refrehFromSystem); +} + +MdColorMgr::ColorSchemeEnum MdColorMgr::sysColorScheme() const { + return convert_from(Xdp::insance()->colorScheme()); +} +QColor MdColorMgr::sysAccentColor() const { return Xdp::insance()->accentColor(); } + +MdColorMgr::ColorSchemeEnum MdColorMgr::colorScheme() const { return m_color_scheme; } +void MdColorMgr::set_colorScheme(ColorSchemeEnum v) { + if (std::exchange(m_color_scheme, v) != v) { + emit colorSchemeChanged(); + } +} + +QColor MdColorMgr::accentColor() const { return m_accent_color; } + +bool MdColorMgr::useSysColorSM() const { return m_use_sys_color_scheme; }; +bool MdColorMgr::useSysAccentColor() const { return m_use_sys_accent_color; }; + +void MdColorMgr::set_accentColor(QColor v) { + if (std::exchange(m_accent_color, v) != v) { + emit accentColorChanged(); + } +} + +void MdColorMgr::set_useSysColorSM(bool v) { + if (std::exchange(m_use_sys_color_scheme, v) != v) { + emit useSysColorSMChanged(); + } +} + +void MdColorMgr::set_useSysAccentColor(bool v) { + if (std::exchange(m_use_sys_accent_color, v) != v) { + emit useSysAccentColorChanged(); + } +} + +QColor MdColorMgr::getOn(QColor in) const { + if (m_on_map.contains(in)) { + return m_on_map.at(in); + } + return m_scheme.on_background; +} + +void MdColorMgr::gen_scheme() { + auto cs = colorScheme(); + if (cs == ColorSchemeEnum::Light) + m_scheme = MaterialLightColorScheme(m_accent_color.rgb()); + else + m_scheme = MaterialDarkColorScheme(m_accent_color.rgb()); + + m_on_map = gen_on_map(m_scheme); + emit schemeChanged(); +} + +void MdColorMgr::refrehFromSystem() { + if (useSysColorSM()) { + set_colorScheme(sysColorScheme()); + } + + if (useSysAccentColor()) { + set_accentColor(sysAccentColor()); + } +} diff --git a/src/corner.cpp b/src/corner.cpp new file mode 100644 index 0000000..7b17056 --- /dev/null +++ b/src/corner.cpp @@ -0,0 +1,56 @@ +#include "qml_material/corner.h" + +using namespace qml_material; + +CornersGroup::CornersGroup(): CornersGroup(0) {} +CornersGroup::CornersGroup(qreal r): CornersGroup(r, r, r, r) {} +CornersGroup::CornersGroup(qreal bottomRight, qreal topRight, qreal bottomLeft, qreal topLeft) + : m_bottomRight(bottomRight), + m_topRight(topRight), + m_bottomLeft(bottomLeft), + m_topLeft(topLeft) {} +CornersGroup::~CornersGroup() {} + +qreal CornersGroup::topLeft() const { return m_topLeft; } + +void CornersGroup::setTopLeft(qreal newTopLeft) { + if (newTopLeft == m_topLeft) { + return; + } + + m_topLeft = newTopLeft; +} + +qreal CornersGroup::topRight() const { return m_topRight; } + +void CornersGroup::setTopRight(qreal newTopRight) { + if (newTopRight == m_topRight) { + return; + } + + m_topRight = newTopRight; +} + +qreal CornersGroup::bottomLeft() const { return m_bottomLeft; } + +void CornersGroup::setBottomLeft(qreal newBottomLeft) { + if (newBottomLeft == m_bottomLeft) { + return; + } + + m_bottomLeft = newBottomLeft; +} + +qreal CornersGroup::bottomRight() const { return m_bottomRight; } + +void CornersGroup::setBottomRight(qreal newBottomRight) { + if (newBottomRight == m_bottomRight) { + return; + } + + m_bottomRight = newBottomRight; +} + +QVector4D CornersGroup::toVector4D() const { + return QVector4D { m_bottomRight, m_topRight, m_bottomLeft, m_topLeft }; +} diff --git a/src/helper.cpp b/src/helper.cpp new file mode 100644 index 0000000..6c3f7ea --- /dev/null +++ b/src/helper.cpp @@ -0,0 +1,100 @@ +#include "qml_material/helper.h" + +#include "cpp/scheme/scheme.h" +#include "cpp/blend/blend.h" + +#include "core/core.h" + +#include "cpp/palettes/core.h" + +using MdScheme = qcm::MdScheme; +namespace md = material_color_utilities; + +namespace +{ + +QRgb blend(QRgb a, QRgb b, double t) { + double dt = 1.0 - t; + return qRgba(qRed(a) * dt + qRed(b) * t, + qGreen(a) * dt + qGreen(b) * t, + qBlue(a) * dt + qBlue(b) * t, + qAlpha(a)); +} +} // namespace + +DEFINE_CONVERT(MdScheme, md::Scheme) { + out.primary = in.primary; + out.on_primary = in.on_primary; + out.primary_container = in.primary_container; + out.on_primary_container = in.on_primary_container; + out.secondary = in.secondary; + out.on_secondary = in.on_secondary; + out.secondary_container = in.secondary_container; + out.on_secondary_container = in.on_secondary_container; + out.tertiary = in.tertiary; + out.on_tertiary = in.on_tertiary; + out.tertiary_container = in.tertiary_container; + out.on_tertiary_container = in.on_tertiary_container; + out.error = in.error; + out.on_error = in.on_error; + out.error_container = in.error_container; + out.on_error_container = in.on_error_container; + out.background = in.background; + out.on_background = in.on_background; + out.surface = in.surface; + out.on_surface = in.on_surface; + out.surface_variant = in.surface_variant; + out.on_surface_variant = in.on_surface_variant; + out.outline = in.outline; + out.outline_variant = in.outline_variant; + out.shadow = in.shadow; + out.scrim = in.scrim; + out.inverse_surface = in.inverse_surface; + out.inverse_on_surface = in.inverse_on_surface; + out.inverse_primary = in.inverse_primary; + out.surface_1 = blend(out.surface, out.primary, 0.05); + out.surface_2 = blend(out.surface, out.primary, 0.08); + out.surface_3 = blend(out.surface, out.primary, 0.11); + out.surface_4 = blend(out.surface, out.primary, 0.12); + out.surface_5 = blend(out.surface, out.primary, 0.14); +} + +MdScheme qcm::MaterialLightColorScheme(QRgb rgb) { + auto palette = md::CorePalette::Of(rgb); + auto scheme = convert_from(md::MaterialLightColorSchemeFromPalette(palette)); + scheme.background = palette.neutral().get(98); + scheme.on_background = palette.neutral().get(10); + + scheme.surface = palette.neutral().get(98); + scheme.surface_dim = palette.neutral().get(87); + scheme.surface_bright = palette.neutral().get(98); + scheme.surface_container = palette.neutral().get(94); + scheme.surface_container_low = palette.neutral().get(96); + scheme.surface_container_lowest = palette.neutral().get(100); + scheme.surface_container_high = palette.neutral().get(92); + scheme.surface_container_highest = palette.neutral().get(90); + + return scheme; +} + +MdScheme qcm::MaterialDarkColorScheme(QRgb rgb) { + auto palette = md::CorePalette::Of(rgb); + auto scheme = convert_from(md::MaterialDarkColorSchemeFromPalette(palette)); + scheme.background = palette.neutral().get(6); + scheme.on_background = palette.neutral().get(90); + + scheme.surface = palette.neutral().get(6); + scheme.surface_dim = palette.neutral().get(6); + scheme.surface_bright = palette.neutral().get(24); + scheme.surface_container = palette.neutral().get(12); + scheme.surface_container_low = palette.neutral().get(10); + scheme.surface_container_lowest = palette.neutral().get(4); + scheme.surface_container_high = palette.neutral().get(17); + scheme.surface_container_highest = palette.neutral().get(22); + + return scheme; +} + +QRgb qcm::MaterialBlendHctHue(const QRgb design_color, const QRgb key_color, const double mount) { + return md::BlendHctHue(design_color, key_color, mount); +} diff --git a/src/icon.cpp b/src/icon.cpp new file mode 100644 index 0000000..0353732 --- /dev/null +++ b/src/icon.cpp @@ -0,0 +1,2285 @@ +#include "qml_material/icon.h" + +namespace qml_material +{ + +static const QVariantMap code_map { { "123", "\ueb8d" }, + { "360", "\ue577" }, + { "10k", "\ue951" }, + { "10mp", "\ue952" }, + { "11mp", "\ue953" }, + { "12mp", "\ue954" }, + { "13mp", "\ue955" }, + { "14mp", "\ue956" }, + { "15mp", "\ue957" }, + { "16mp", "\ue958" }, + { "17mp", "\ue959" }, + { "18_up_rating", "\uf8fd" }, + { "18mp", "\ue95a" }, + { "19mp", "\ue95b" }, + { "1k", "\ue95c" }, + { "1k_plus", "\ue95d" }, + { "1x_mobiledata", "\uefcd" }, + { "20mp", "\ue95e" }, + { "21mp", "\ue95f" }, + { "22mp", "\ue960" }, + { "23mp", "\ue961" }, + { "24mp", "\ue962" }, + { "2k", "\ue963" }, + { "2k_plus", "\ue964" }, + { "2mp", "\ue965" }, + { "30fps", "\uefce" }, + { "30fps_select", "\uefcf" }, + { "3d_rotation", "\ue84d" }, + { "3g_mobiledata", "\uefd0" }, + { "3k", "\ue966" }, + { "3k_plus", "\ue967" }, + { "3mp", "\ue968" }, + { "3p", "\uefd1" }, + { "4g_mobiledata", "\uefd2" }, + { "4g_plus_mobiledata", "\uefd3" }, + { "4k", "\ue072" }, + { "4k_plus", "\ue969" }, + { "4mp", "\ue96a" }, + { "5g", "\uef38" }, + { "5k", "\ue96b" }, + { "5k_plus", "\ue96c" }, + { "5mp", "\ue96d" }, + { "60fps", "\uefd4" }, + { "60fps_select", "\uefd5" }, + { "6_ft_apart", "\uf21e" }, + { "6k", "\ue96e" }, + { "6k_plus", "\ue96f" }, + { "6mp", "\ue970" }, + { "7k", "\ue971" }, + { "7k_plus", "\ue972" }, + { "7mp", "\ue973" }, + { "8k", "\ue974" }, + { "8k_plus", "\ue975" }, + { "8mp", "\ue976" }, + { "9k", "\ue977" }, + { "9k_plus", "\ue978" }, + { "9mp", "\ue979" }, + { "abc", "\ueb94" }, + { "ac_unit", "\ueb3b" }, + { "access_alarm", "\ue190" }, + { "access_alarms", "\ue191" }, + { "access_time", "\ue192" }, + { "access_time_filled", "\uefd6" }, + { "accessibility", "\ue84e" }, + { "accessibility_new", "\ue92c" }, + { "accessible", "\ue914" }, + { "accessible_forward", "\ue934" }, + { "account_balance", "\ue84f" }, + { "account_balance_wallet", "\ue850" }, + { "account_box", "\ue851" }, + { "account_circle", "\ue853" }, + { "account_tree", "\ue97a" }, + { "ad_units", "\uef39" }, + { "adb", "\ue60e" }, + { "add", "\ue145" }, + { "add_a_photo", "\ue439" }, + { "add_alarm", "\ue193" }, + { "add_alert", "\ue003" }, + { "add_box", "\ue146" }, + { "add_business", "\ue729" }, + { "add_call", "\ue0e8" }, + { "add_card", "\ueb86" }, + { "add_chart", "\ue97b" }, + { "add_circle", "\ue147" }, + { "add_circle_outline", "\ue148" }, + { "add_comment", "\ue266" }, + { "add_home", "\uf8eb" }, + { "add_home_work", "\uf8ed" }, + { "add_ic_call", "\ue97c" }, + { "add_link", "\ue178" }, + { "add_location", "\ue567" }, + { "add_location_alt", "\uef3a" }, + { "add_moderator", "\ue97d" }, + { "add_photo_alternate", "\ue43e" }, + { "add_reaction", "\ue1d3" }, + { "add_road", "\uef3b" }, + { "add_shopping_cart", "\ue854" }, + { "add_task", "\uf23a" }, + { "add_to_drive", "\ue65c" }, + { "add_to_home_screen", "\ue1fe" }, + { "add_to_photos", "\ue39d" }, + { "add_to_queue", "\ue05c" }, + { "addchart", "\uef3c" }, + { "adf_scanner", "\ueada" }, + { "adjust", "\ue39e" }, + { "admin_panel_settings", "\uef3d" }, + { "adobe", "\uea96" }, + { "ads_click", "\ue762" }, + { "agriculture", "\uea79" }, + { "air", "\uefd8" }, + { "airline_seat_flat", "\ue630" }, + { "airline_seat_flat_angled", "\ue631" }, + { "airline_seat_individual_suite", "\ue632" }, + { "airline_seat_legroom_extra", "\ue633" }, + { "airline_seat_legroom_normal", "\ue634" }, + { "airline_seat_legroom_reduced", "\ue635" }, + { "airline_seat_recline_extra", "\ue636" }, + { "airline_seat_recline_normal", "\ue637" }, + { "airline_stops", "\ue7d0" }, + { "airlines", "\ue7ca" }, + { "airplane_ticket", "\uefd9" }, + { "airplanemode_active", "\ue195" }, + { "airplanemode_inactive", "\ue194" }, + { "airplanemode_off", "\ue194" }, + { "airplanemode_on", "\ue195" }, + { "airplay", "\ue055" }, + { "airport_shuttle", "\ueb3c" }, + { "alarm", "\ue855" }, + { "alarm_add", "\ue856" }, + { "alarm_off", "\ue857" }, + { "alarm_on", "\ue858" }, + { "album", "\ue019" }, + { "align_horizontal_center", "\ue00f" }, + { "align_horizontal_left", "\ue00d" }, + { "align_horizontal_right", "\ue010" }, + { "align_vertical_bottom", "\ue015" }, + { "align_vertical_center", "\ue011" }, + { "align_vertical_top", "\ue00c" }, + { "all_inbox", "\ue97f" }, + { "all_inclusive", "\ueb3d" }, + { "all_out", "\ue90b" }, + { "alt_route", "\uf184" }, + { "alternate_email", "\ue0e6" }, + { "amp_stories", "\uea13" }, + { "analytics", "\uef3e" }, + { "anchor", "\uf1cd" }, + { "android", "\ue859" }, + { "animation", "\ue71c" }, + { "announcement", "\ue85a" }, + { "aod", "\uefda" }, + { "apartment", "\uea40" }, + { "api", "\uf1b7" }, + { "app_blocking", "\uef3f" }, + { "app_registration", "\uef40" }, + { "app_settings_alt", "\uef41" }, + { "app_shortcut", "\ueae4" }, + { "apple", "\uea80" }, + { "approval", "\ue982" }, + { "apps", "\ue5c3" }, + { "apps_outage", "\ue7cc" }, + { "architecture", "\uea3b" }, + { "archive", "\ue149" }, + { "area_chart", "\ue770" }, + { "arrow_back", "\ue5c4" }, + { "arrow_back_ios", "\ue5e0" }, + { "arrow_back_ios_new", "\ue2ea" }, + { "arrow_circle_down", "\uf181" }, + { "arrow_circle_left", "\ueaa7" }, + { "arrow_circle_right", "\ueaaa" }, + { "arrow_circle_up", "\uf182" }, + { "arrow_downward", "\ue5db" }, + { "arrow_drop_down", "\ue5c5" }, + { "arrow_drop_down_circle", "\ue5c6" }, + { "arrow_drop_up", "\ue5c7" }, + { "arrow_forward", "\ue5c8" }, + { "arrow_forward_ios", "\ue5e1" }, + { "arrow_left", "\ue5de" }, + { "arrow_outward", "\uf8ce" }, + { "arrow_right", "\ue5df" }, + { "arrow_right_alt", "\ue941" }, + { "arrow_upward", "\ue5d8" }, + { "art_track", "\ue060" }, + { "article", "\uef42" }, + { "aspect_ratio", "\ue85b" }, + { "assessment", "\ue85c" }, + { "assignment", "\ue85d" }, + { "assignment_add", "\uf848" }, + { "assignment_ind", "\ue85e" }, + { "assignment_late", "\ue85f" }, + { "assignment_return", "\ue860" }, + { "assignment_returned", "\ue861" }, + { "assignment_turned_in", "\ue862" }, + { "assist_walker", "\uf8d5" }, + { "assistant", "\ue39f" }, + { "assistant_direction", "\ue988" }, + { "assistant_navigation", "\ue989" }, + { "assistant_photo", "\ue3a0" }, + { "assured_workload", "\ueb6f" }, + { "atm", "\ue573" }, + { "attach_email", "\uea5e" }, + { "attach_file", "\ue226" }, + { "attach_money", "\ue227" }, + { "attachment", "\ue2bc" }, + { "attractions", "\uea52" }, + { "attribution", "\uefdb" }, + { "audio_file", "\ueb82" }, + { "audiotrack", "\ue3a1" }, + { "auto_awesome", "\ue65f" }, + { "auto_awesome_mosaic", "\ue660" }, + { "auto_awesome_motion", "\ue661" }, + { "auto_delete", "\uea4c" }, + { "auto_fix_high", "\ue663" }, + { "auto_fix_normal", "\ue664" }, + { "auto_fix_off", "\ue665" }, + { "auto_graph", "\ue4fb" }, + { "auto_mode", "\uec20" }, + { "auto_stories", "\ue666" }, + { "autofps_select", "\uefdc" }, + { "autorenew", "\ue863" }, + { "av_timer", "\ue01b" }, + { "baby_changing_station", "\uf19b" }, + { "back_hand", "\ue764" }, + { "backpack", "\uf19c" }, + { "backspace", "\ue14a" }, + { "backup", "\ue864" }, + { "backup_table", "\uef43" }, + { "badge", "\uea67" }, + { "bakery_dining", "\uea53" }, + { "balance", "\ueaf6" }, + { "balcony", "\ue58f" }, + { "ballot", "\ue172" }, + { "bar_chart", "\ue26b" }, + { "barcode_reader", "\uf85c" }, + { "batch_prediction", "\uf0f5" }, + { "bathroom", "\uefdd" }, + { "bathtub", "\uea41" }, + { "battery_0_bar", "\uebdc" }, + { "battery_1_bar", "\uebd9" }, + { "battery_20", "\uf09c" }, + { "battery_2_bar", "\uebe0" }, + { "battery_30", "\uf09d" }, + { "battery_3_bar", "\uebdd" }, + { "battery_4_bar", "\uebe2" }, + { "battery_50", "\uf09e" }, + { "battery_5_bar", "\uebd4" }, + { "battery_60", "\uf09f" }, + { "battery_6_bar", "\uebd2" }, + { "battery_80", "\uf0a0" }, + { "battery_90", "\uf0a1" }, + { "battery_alert", "\ue19c" }, + { "battery_charging_20", "\uf0a2" }, + { "battery_charging_30", "\uf0a3" }, + { "battery_charging_50", "\uf0a4" }, + { "battery_charging_60", "\uf0a5" }, + { "battery_charging_80", "\uf0a6" }, + { "battery_charging_90", "\uf0a7" }, + { "battery_charging_full", "\ue1a3" }, + { "battery_full", "\ue1a4" }, + { "battery_saver", "\uefde" }, + { "battery_std", "\ue1a5" }, + { "battery_unknown", "\ue1a6" }, + { "beach_access", "\ueb3e" }, + { "bed", "\uefdf" }, + { "bedroom_baby", "\uefe0" }, + { "bedroom_child", "\uefe1" }, + { "bedroom_parent", "\uefe2" }, + { "bedtime", "\uef44" }, + { "bedtime_off", "\ueb76" }, + { "beenhere", "\ue52d" }, + { "bento", "\uf1f4" }, + { "bike_scooter", "\uef45" }, + { "biotech", "\uea3a" }, + { "blender", "\uefe3" }, + { "blind", "\uf8d6" }, + { "blinds", "\ue286" }, + { "blinds_closed", "\uec1f" }, + { "block", "\ue14b" }, + { "block_flipped", "\uef46" }, + { "bloodtype", "\uefe4" }, + { "bluetooth", "\ue1a7" }, + { "bluetooth_audio", "\ue60f" }, + { "bluetooth_connected", "\ue1a8" }, + { "bluetooth_disabled", "\ue1a9" }, + { "bluetooth_drive", "\uefe5" }, + { "bluetooth_searching", "\ue1aa" }, + { "blur_circular", "\ue3a2" }, + { "blur_linear", "\ue3a3" }, + { "blur_off", "\ue3a4" }, + { "blur_on", "\ue3a5" }, + { "bolt", "\uea0b" }, + { "book", "\ue865" }, + { "book_online", "\uf217" }, + { "bookmark", "\ue866" }, + { "bookmark_add", "\ue598" }, + { "bookmark_added", "\ue599" }, + { "bookmark_border", "\ue867" }, + { "bookmark_outline", "\ue867" }, + { "bookmark_remove", "\ue59a" }, + { "bookmarks", "\ue98b" }, + { "border_all", "\ue228" }, + { "border_bottom", "\ue229" }, + { "border_clear", "\ue22a" }, + { "border_color", "\ue22b" }, + { "border_horizontal", "\ue22c" }, + { "border_inner", "\ue22d" }, + { "border_left", "\ue22e" }, + { "border_outer", "\ue22f" }, + { "border_right", "\ue230" }, + { "border_style", "\ue231" }, + { "border_top", "\ue232" }, + { "border_vertical", "\ue233" }, + { "boy", "\ueb67" }, + { "branding_watermark", "\ue06b" }, + { "breakfast_dining", "\uea54" }, + { "brightness_1", "\ue3a6" }, + { "brightness_2", "\ue3a7" }, + { "brightness_3", "\ue3a8" }, + { "brightness_4", "\ue3a9" }, + { "brightness_5", "\ue3aa" }, + { "brightness_6", "\ue3ab" }, + { "brightness_7", "\ue3ac" }, + { "brightness_auto", "\ue1ab" }, + { "brightness_high", "\ue1ac" }, + { "brightness_low", "\ue1ad" }, + { "brightness_medium", "\ue1ae" }, + { "broadcast_on_home", "\uf8f8" }, + { "broadcast_on_personal", "\uf8f9" }, + { "broken_image", "\ue3ad" }, + { "browse_gallery", "\uebd1" }, + { "browser_not_supported", "\uef47" }, + { "browser_updated", "\ue7cf" }, + { "brunch_dining", "\uea73" }, + { "brush", "\ue3ae" }, + { "bubble_chart", "\ue6dd" }, + { "bug_report", "\ue868" }, + { "build", "\ue869" }, + { "build_circle", "\uef48" }, + { "bungalow", "\ue591" }, + { "burst_mode", "\ue43c" }, + { "bus_alert", "\ue98f" }, + { "business", "\ue0af" }, + { "business_center", "\ueb3f" }, + { "cabin", "\ue589" }, + { "cable", "\uefe6" }, + { "cached", "\ue86a" }, + { "cake", "\ue7e9" }, + { "calculate", "\uea5f" }, + { "calendar_month", "\uebcc" }, + { "calendar_today", "\ue935" }, + { "calendar_view_day", "\ue936" }, + { "calendar_view_month", "\uefe7" }, + { "calendar_view_week", "\uefe8" }, + { "call", "\ue0b0" }, + { "call_end", "\ue0b1" }, + { "call_made", "\ue0b2" }, + { "call_merge", "\ue0b3" }, + { "call_missed", "\ue0b4" }, + { "call_missed_outgoing", "\ue0e4" }, + { "call_received", "\ue0b5" }, + { "call_split", "\ue0b6" }, + { "call_to_action", "\ue06c" }, + { "camera", "\ue3af" }, + { "camera_alt", "\ue3b0" }, + { "camera_enhance", "\ue8fc" }, + { "camera_front", "\ue3b1" }, + { "camera_indoor", "\uefe9" }, + { "camera_outdoor", "\uefea" }, + { "camera_rear", "\ue3b2" }, + { "camera_roll", "\ue3b3" }, + { "cameraswitch", "\uefeb" }, + { "campaign", "\uef49" }, + { "cancel", "\ue5c9" }, + { "cancel_presentation", "\ue0e9" }, + { "cancel_schedule_send", "\uea39" }, + { "candlestick_chart", "\uead4" }, + { "car_crash", "\uebf2" }, + { "car_rental", "\uea55" }, + { "car_repair", "\uea56" }, + { "card_giftcard", "\ue8f6" }, + { "card_membership", "\ue8f7" }, + { "card_travel", "\ue8f8" }, + { "carpenter", "\uf1f8" }, + { "cases", "\ue992" }, + { "casino", "\ueb40" }, + { "cast", "\ue307" }, + { "cast_connected", "\ue308" }, + { "cast_for_education", "\uefec" }, + { "castle", "\ueab1" }, + { "catching_pokemon", "\ue508" }, + { "category", "\ue574" }, + { "celebration", "\uea65" }, + { "cell_tower", "\uebba" }, + { "cell_wifi", "\ue0ec" }, + { "center_focus_strong", "\ue3b4" }, + { "center_focus_weak", "\ue3b5" }, + { "chair", "\uefed" }, + { "chair_alt", "\uefee" }, + { "chalet", "\ue585" }, + { "change_circle", "\ue2e7" }, + { "change_history", "\ue86b" }, + { "charging_station", "\uf19d" }, + { "chat", "\ue0b7" }, + { "chat_bubble", "\ue0ca" }, + { "chat_bubble_outline", "\ue0cb" }, + { "check", "\ue5ca" }, + { "check_box", "\ue834" }, + { "check_box_outline_blank", "\ue835" }, + { "check_circle", "\ue86c" }, + { "check_circle_outline", "\ue92d" }, + { "checklist", "\ue6b1" }, + { "checklist_rtl", "\ue6b3" }, + { "checkroom", "\uf19e" }, + { "chevron_left", "\ue5cb" }, + { "chevron_right", "\ue5cc" }, + { "child_care", "\ueb41" }, + { "child_friendly", "\ueb42" }, + { "chrome_reader_mode", "\ue86d" }, + { "church", "\ueaae" }, + { "circle", "\uef4a" }, + { "circle_notifications", "\ue994" }, + { "class", "\ue86e" }, + { "clean_hands", "\uf21f" }, + { "cleaning_services", "\uf0ff" }, + { "clear", "\ue14c" }, + { "clear_all", "\ue0b8" }, + { "close", "\ue5cd" }, + { "close_fullscreen", "\uf1cf" }, + { "closed_caption", "\ue01c" }, + { "closed_caption_disabled", "\uf1dc" }, + { "closed_caption_off", "\ue996" }, + { "cloud", "\ue2bd" }, + { "cloud_circle", "\ue2be" }, + { "cloud_done", "\ue2bf" }, + { "cloud_download", "\ue2c0" }, + { "cloud_off", "\ue2c1" }, + { "cloud_queue", "\ue2c2" }, + { "cloud_sync", "\ueb5a" }, + { "cloud_upload", "\ue2c3" }, + { "cloudy_snowing", "\ue810" }, + { "co2", "\ue7b0" }, + { "co_present", "\ueaf0" }, + { "code", "\ue86f" }, + { "code_off", "\ue4f3" }, + { "coffee", "\uefef" }, + { "coffee_maker", "\ueff0" }, + { "collections", "\ue3b6" }, + { "collections_bookmark", "\ue431" }, + { "color_lens", "\ue3b7" }, + { "colorize", "\ue3b8" }, + { "comment", "\ue0b9" }, + { "comment_bank", "\uea4e" }, + { "comments_disabled", "\ue7a2" }, + { "commit", "\ueaf5" }, + { "commute", "\ue940" }, + { "compare", "\ue3b9" }, + { "compare_arrows", "\ue915" }, + { "compass_calibration", "\ue57c" }, + { "compost", "\ue761" }, + { "compress", "\ue94d" }, + { "computer", "\ue30a" }, + { "confirmation_num", "\ue638" }, + { "confirmation_number", "\ue638" }, + { "connect_without_contact", "\uf223" }, + { "connected_tv", "\ue998" }, + { "connecting_airports", "\ue7c9" }, + { "construction", "\uea3c" }, + { "contact_emergency", "\uf8d1" }, + { "contact_mail", "\ue0d0" }, + { "contact_page", "\uf22e" }, + { "contact_phone", "\ue0cf" }, + { "contact_support", "\ue94c" }, + { "contactless", "\uea71" }, + { "contacts", "\ue0ba" }, + { "content_copy", "\ue14d" }, + { "content_cut", "\ue14e" }, + { "content_paste", "\ue14f" }, + { "content_paste_go", "\uea8e" }, + { "content_paste_off", "\ue4f8" }, + { "content_paste_search", "\uea9b" }, + { "contrast", "\ueb37" }, + { "control_camera", "\ue074" }, + { "control_point", "\ue3ba" }, + { "control_point_duplicate", "\ue3bb" }, + { "conveyor_belt", "\uf867" }, + { "cookie", "\ueaac" }, + { "copy", "\uf08a" }, + { "copy_all", "\ue2ec" }, + { "copyright", "\ue90c" }, + { "coronavirus", "\uf221" }, + { "corporate_fare", "\uf1d0" }, + { "cottage", "\ue587" }, + { "countertops", "\uf1f7" }, + { "create", "\ue150" }, + { "create_new_folder", "\ue2cc" }, + { "credit_card", "\ue870" }, + { "credit_card_off", "\ue4f4" }, + { "credit_score", "\ueff1" }, + { "crib", "\ue588" }, + { "crisis_alert", "\uebe9" }, + { "crop", "\ue3be" }, + { "crop_16_9", "\ue3bc" }, + { "crop_3_2", "\ue3bd" }, + { "crop_5_4", "\ue3bf" }, + { "crop_7_5", "\ue3c0" }, + { "crop_din", "\ue3c1" }, + { "crop_free", "\ue3c2" }, + { "crop_landscape", "\ue3c3" }, + { "crop_original", "\ue3c4" }, + { "crop_portrait", "\ue3c5" }, + { "crop_rotate", "\ue437" }, + { "crop_square", "\ue3c6" }, + { "cruelty_free", "\ue799" }, + { "css", "\ueb93" }, + { "currency_bitcoin", "\uebc5" }, + { "currency_exchange", "\ueb70" }, + { "currency_franc", "\ueafa" }, + { "currency_lira", "\ueaef" }, + { "currency_pound", "\ueaf1" }, + { "currency_ruble", "\ueaec" }, + { "currency_rupee", "\ueaf7" }, + { "currency_yen", "\ueafb" }, + { "currency_yuan", "\ueaf9" }, + { "curtains", "\uec1e" }, + { "curtains_closed", "\uec1d" }, + { "cut", "\uf08b" }, + { "cyclone", "\uebd5" }, + { "dangerous", "\ue99a" }, + { "dark_mode", "\ue51c" }, + { "dashboard", "\ue871" }, + { "dashboard_customize", "\ue99b" }, + { "data_array", "\uead1" }, + { "data_exploration", "\ue76f" }, + { "data_object", "\uead3" }, + { "data_saver_off", "\ueff2" }, + { "data_saver_on", "\ueff3" }, + { "data_thresholding", "\ueb9f" }, + { "data_usage", "\ue1af" }, + { "dataset", "\uf8ee" }, + { "dataset_linked", "\uf8ef" }, + { "date_range", "\ue916" }, + { "deblur", "\ueb77" }, + { "deck", "\uea42" }, + { "dehaze", "\ue3c7" }, + { "delete", "\ue872" }, + { "delete_forever", "\ue92b" }, + { "delete_outline", "\ue92e" }, + { "delete_sweep", "\ue16c" }, + { "delivery_dining", "\uea72" }, + { "density_large", "\ueba9" }, + { "density_medium", "\ueb9e" }, + { "density_small", "\ueba8" }, + { "departure_board", "\ue576" }, + { "description", "\ue873" }, + { "deselect", "\uebb6" }, + { "design_services", "\uf10a" }, + { "desk", "\uf8f4" }, + { "desktop_access_disabled", "\ue99d" }, + { "desktop_mac", "\ue30b" }, + { "desktop_windows", "\ue30c" }, + { "details", "\ue3c8" }, + { "developer_board", "\ue30d" }, + { "developer_board_off", "\ue4ff" }, + { "developer_mode", "\ue1b0" }, + { "device_hub", "\ue335" }, + { "device_thermostat", "\ue1ff" }, + { "device_unknown", "\ue339" }, + { "devices", "\ue1b1" }, + { "devices_fold", "\uebde" }, + { "devices_other", "\ue337" }, + { "dew_point", "\uf879" }, + { "dialer_sip", "\ue0bb" }, + { "dialpad", "\ue0bc" }, + { "diamond", "\uead5" }, + { "difference", "\ueb7d" }, + { "dining", "\ueff4" }, + { "dinner_dining", "\uea57" }, + { "directions", "\ue52e" }, + { "directions_bike", "\ue52f" }, + { "directions_boat", "\ue532" }, + { "directions_boat_filled", "\ueff5" }, + { "directions_bus", "\ue530" }, + { "directions_bus_filled", "\ueff6" }, + { "directions_car", "\ue531" }, + { "directions_car_filled", "\ueff7" }, + { "directions_ferry", "\ue532" }, + { "directions_off", "\uf10f" }, + { "directions_railway", "\ue534" }, + { "directions_railway_filled", "\ueff8" }, + { "directions_run", "\ue566" }, + { "directions_subway", "\ue533" }, + { "directions_subway_filled", "\ueff9" }, + { "directions_train", "\ue534" }, + { "directions_transit", "\ue535" }, + { "directions_transit_filled", "\ueffa" }, + { "directions_walk", "\ue536" }, + { "dirty_lens", "\uef4b" }, + { "disabled_by_default", "\uf230" }, + { "disabled_visible", "\ue76e" }, + { "disc_full", "\ue610" }, + { "discord", "\uea6c" }, + { "discount", "\uebc9" }, + { "display_settings", "\ueb97" }, + { "diversity_1", "\uf8d7" }, + { "diversity_2", "\uf8d8" }, + { "diversity_3", "\uf8d9" }, + { "dnd_forwardslash", "\ue611" }, + { "dns", "\ue875" }, + { "do_disturb", "\uf08c" }, + { "do_disturb_alt", "\uf08d" }, + { "do_disturb_off", "\uf08e" }, + { "do_disturb_on", "\uf08f" }, + { "do_not_disturb", "\ue612" }, + { "do_not_disturb_alt", "\ue611" }, + { "do_not_disturb_off", "\ue643" }, + { "do_not_disturb_on", "\ue644" }, + { "do_not_disturb_on_total_silence", "\ueffb" }, + { "do_not_step", "\uf19f" }, + { "do_not_touch", "\uf1b0" }, + { "dock", "\ue30e" }, + { "document_scanner", "\ue5fa" }, + { "domain", "\ue7ee" }, + { "domain_add", "\ueb62" }, + { "domain_disabled", "\ue0ef" }, + { "domain_verification", "\uef4c" }, + { "done", "\ue876" }, + { "done_all", "\ue877" }, + { "done_outline", "\ue92f" }, + { "donut_large", "\ue917" }, + { "donut_small", "\ue918" }, + { "door_back", "\ueffc" }, + { "door_front", "\ueffd" }, + { "door_sliding", "\ueffe" }, + { "doorbell", "\uefff" }, + { "double_arrow", "\uea50" }, + { "downhill_skiing", "\ue509" }, + { "download", "\uf090" }, + { "download_done", "\uf091" }, + { "download_for_offline", "\uf000" }, + { "downloading", "\uf001" }, + { "drafts", "\ue151" }, + { "drag_handle", "\ue25d" }, + { "drag_indicator", "\ue945" }, + { "draw", "\ue746" }, + { "drive_eta", "\ue613" }, + { "drive_file_move", "\ue675" }, + { "drive_file_move_outline", "\ue9a1" }, + { "drive_file_move_rtl", "\ue76d" }, + { "drive_file_rename_outline", "\ue9a2" }, + { "drive_folder_upload", "\ue9a3" }, + { "dry", "\uf1b3" }, + { "dry_cleaning", "\uea58" }, + { "duo", "\ue9a5" }, + { "dvr", "\ue1b2" }, + { "dynamic_feed", "\uea14" }, + { "dynamic_form", "\uf1bf" }, + { "e_mobiledata", "\uf002" }, + { "earbuds", "\uf003" }, + { "earbuds_battery", "\uf004" }, + { "east", "\uf1df" }, + { "eco", "\uea35" }, + { "edgesensor_high", "\uf005" }, + { "edgesensor_low", "\uf006" }, + { "edit", "\ue3c9" }, + { "edit_attributes", "\ue578" }, + { "edit_calendar", "\ue742" }, + { "edit_document", "\uf88c" }, + { "edit_location", "\ue568" }, + { "edit_location_alt", "\ue1c5" }, + { "edit_note", "\ue745" }, + { "edit_notifications", "\ue525" }, + { "edit_off", "\ue950" }, + { "edit_road", "\uef4d" }, + { "edit_square", "\uf88d" }, + { "egg", "\ueacc" }, + { "egg_alt", "\ueac8" }, + { "eject", "\ue8fb" }, + { "elderly", "\uf21a" }, + { "elderly_woman", "\ueb69" }, + { "electric_bike", "\ueb1b" }, + { "electric_bolt", "\uec1c" }, + { "electric_car", "\ueb1c" }, + { "electric_meter", "\uec1b" }, + { "electric_moped", "\ueb1d" }, + { "electric_rickshaw", "\ueb1e" }, + { "electric_scooter", "\ueb1f" }, + { "electrical_services", "\uf102" }, + { "elevator", "\uf1a0" }, + { "email", "\ue0be" }, + { "emergency", "\ue1eb" }, + { "emergency_recording", "\uebf4" }, + { "emergency_share", "\uebf6" }, + { "emoji_emotions", "\uea22" }, + { "emoji_events", "\uea23" }, + { "emoji_flags", "\uea1a" }, + { "emoji_food_beverage", "\uea1b" }, + { "emoji_nature", "\uea1c" }, + { "emoji_objects", "\uea24" }, + { "emoji_people", "\uea1d" }, + { "emoji_symbols", "\uea1e" }, + { "emoji_transportation", "\uea1f" }, + { "energy_savings_leaf", "\uec1a" }, + { "engineering", "\uea3d" }, + { "enhance_photo_translate", "\ue8fc" }, + { "enhanced_encryption", "\ue63f" }, + { "equalizer", "\ue01d" }, + { "error", "\ue000" }, + { "error_outline", "\ue001" }, + { "escalator", "\uf1a1" }, + { "escalator_warning", "\uf1ac" }, + { "euro", "\uea15" }, + { "euro_symbol", "\ue926" }, + { "ev_station", "\ue56d" }, + { "event", "\ue878" }, + { "event_available", "\ue614" }, + { "event_busy", "\ue615" }, + { "event_note", "\ue616" }, + { "event_repeat", "\ueb7b" }, + { "event_seat", "\ue903" }, + { "exit_to_app", "\ue879" }, + { "expand", "\ue94f" }, + { "expand_circle_down", "\ue7cd" }, + { "expand_less", "\ue5ce" }, + { "expand_more", "\ue5cf" }, + { "explicit", "\ue01e" }, + { "explore", "\ue87a" }, + { "explore_off", "\ue9a8" }, + { "exposure", "\ue3ca" }, + { "exposure_minus_1", "\ue3cb" }, + { "exposure_minus_2", "\ue3cc" }, + { "exposure_neg_1", "\ue3cb" }, + { "exposure_neg_2", "\ue3cc" }, + { "exposure_plus_1", "\ue3cd" }, + { "exposure_plus_2", "\ue3ce" }, + { "exposure_zero", "\ue3cf" }, + { "extension", "\ue87b" }, + { "extension_off", "\ue4f5" }, + { "face", "\ue87c" }, + { "face_2", "\uf8da" }, + { "face_3", "\uf8db" }, + { "face_4", "\uf8dc" }, + { "face_5", "\uf8dd" }, + { "face_6", "\uf8de" }, + { "face_retouching_natural", "\uef4e" }, + { "face_retouching_off", "\uf007" }, + { "face_unlock", "\uf008" }, + { "facebook", "\uf234" }, + { "fact_check", "\uf0c5" }, + { "factory", "\uebbc" }, + { "family_restroom", "\uf1a2" }, + { "fast_forward", "\ue01f" }, + { "fast_rewind", "\ue020" }, + { "fastfood", "\ue57a" }, + { "favorite", "\ue87d" }, + { "favorite_border", "\ue87e" }, + { "favorite_outline", "\ue87e" }, + { "fax", "\uead8" }, + { "featured_play_list", "\ue06d" }, + { "featured_video", "\ue06e" }, + { "feed", "\uf009" }, + { "feedback", "\ue87f" }, + { "female", "\ue590" }, + { "fence", "\uf1f6" }, + { "festival", "\uea68" }, + { "fiber_dvr", "\ue05d" }, + { "fiber_manual_record", "\ue061" }, + { "fiber_new", "\ue05e" }, + { "fiber_pin", "\ue06a" }, + { "fiber_smart_record", "\ue062" }, + { "file_copy", "\ue173" }, + { "file_download", "\ue2c4" }, + { "file_download_done", "\ue9aa" }, + { "file_download_off", "\ue4fe" }, + { "file_open", "\ueaf3" }, + { "file_present", "\uea0e" }, + { "file_upload", "\ue2c6" }, + { "file_upload_off", "\uf886" }, + { "filter", "\ue3d3" }, + { "filter_1", "\ue3d0" }, + { "filter_2", "\ue3d1" }, + { "filter_3", "\ue3d2" }, + { "filter_4", "\ue3d4" }, + { "filter_5", "\ue3d5" }, + { "filter_6", "\ue3d6" }, + { "filter_7", "\ue3d7" }, + { "filter_8", "\ue3d8" }, + { "filter_9", "\ue3d9" }, + { "filter_9_plus", "\ue3da" }, + { "filter_alt", "\uef4f" }, + { "filter_alt_off", "\ueb32" }, + { "filter_b_and_w", "\ue3db" }, + { "filter_center_focus", "\ue3dc" }, + { "filter_drama", "\ue3dd" }, + { "filter_frames", "\ue3de" }, + { "filter_hdr", "\ue3df" }, + { "filter_list", "\ue152" }, + { "filter_list_alt", "\ue94e" }, + { "filter_list_off", "\ueb57" }, + { "filter_none", "\ue3e0" }, + { "filter_tilt_shift", "\ue3e2" }, + { "filter_vintage", "\ue3e3" }, + { "find_in_page", "\ue880" }, + { "find_replace", "\ue881" }, + { "fingerprint", "\ue90d" }, + { "fire_extinguisher", "\uf1d8" }, + { "fire_hydrant", "\uf1a3" }, + { "fire_hydrant_alt", "\uf8f1" }, + { "fire_truck", "\uf8f2" }, + { "fireplace", "\uea43" }, + { "first_page", "\ue5dc" }, + { "fit_screen", "\uea10" }, + { "fitbit", "\ue82b" }, + { "fitness_center", "\ueb43" }, + { "flag", "\ue153" }, + { "flag_circle", "\ueaf8" }, + { "flaky", "\uef50" }, + { "flare", "\ue3e4" }, + { "flash_auto", "\ue3e5" }, + { "flash_off", "\ue3e6" }, + { "flash_on", "\ue3e7" }, + { "flashlight_off", "\uf00a" }, + { "flashlight_on", "\uf00b" }, + { "flatware", "\uf00c" }, + { "flight", "\ue539" }, + { "flight_class", "\ue7cb" }, + { "flight_land", "\ue904" }, + { "flight_takeoff", "\ue905" }, + { "flip", "\ue3e8" }, + { "flip_camera_android", "\uea37" }, + { "flip_camera_ios", "\uea38" }, + { "flip_to_back", "\ue882" }, + { "flip_to_front", "\ue883" }, + { "flood", "\uebe6" }, + { "flourescent", "\uec31" }, + { "fluorescent", "\uec31" }, + { "flutter_dash", "\ue00b" }, + { "fmd_bad", "\uf00e" }, + { "fmd_good", "\uf00f" }, + { "foggy", "\ue818" }, + { "folder", "\ue2c7" }, + { "folder_copy", "\uebbd" }, + { "folder_delete", "\ueb34" }, + { "folder_off", "\ueb83" }, + { "folder_open", "\ue2c8" }, + { "folder_shared", "\ue2c9" }, + { "folder_special", "\ue617" }, + { "folder_zip", "\ueb2c" }, + { "follow_the_signs", "\uf222" }, + { "font_download", "\ue167" }, + { "font_download_off", "\ue4f9" }, + { "food_bank", "\uf1f2" }, + { "forest", "\uea99" }, + { "fork_left", "\ueba0" }, + { "fork_right", "\uebac" }, + { "forklift", "\uf868" }, + { "format_align_center", "\ue234" }, + { "format_align_justify", "\ue235" }, + { "format_align_left", "\ue236" }, + { "format_align_right", "\ue237" }, + { "format_bold", "\ue238" }, + { "format_clear", "\ue239" }, + { "format_color_fill", "\ue23a" }, + { "format_color_reset", "\ue23b" }, + { "format_color_text", "\ue23c" }, + { "format_indent_decrease", "\ue23d" }, + { "format_indent_increase", "\ue23e" }, + { "format_italic", "\ue23f" }, + { "format_line_spacing", "\ue240" }, + { "format_list_bulleted", "\ue241" }, + { "format_list_bulleted_add", "\uf849" }, + { "format_list_numbered", "\ue242" }, + { "format_list_numbered_rtl", "\ue267" }, + { "format_overline", "\ueb65" }, + { "format_paint", "\ue243" }, + { "format_quote", "\ue244" }, + { "format_shapes", "\ue25e" }, + { "format_size", "\ue245" }, + { "format_strikethrough", "\ue246" }, + { "format_textdirection_l_to_r", "\ue247" }, + { "format_textdirection_r_to_l", "\ue248" }, + { "format_underline", "\ue249" }, + { "format_underlined", "\ue249" }, + { "fort", "\ueaad" }, + { "forum", "\ue0bf" }, + { "forward", "\ue154" }, + { "forward_10", "\ue056" }, + { "forward_30", "\ue057" }, + { "forward_5", "\ue058" }, + { "forward_to_inbox", "\uf187" }, + { "foundation", "\uf200" }, + { "free_breakfast", "\ueb44" }, + { "free_cancellation", "\ue748" }, + { "front_hand", "\ue769" }, + { "front_loader", "\uf869" }, + { "fullscreen", "\ue5d0" }, + { "fullscreen_exit", "\ue5d1" }, + { "functions", "\ue24a" }, + { "g_mobiledata", "\uf010" }, + { "g_translate", "\ue927" }, + { "gamepad", "\ue30f" }, + { "games", "\ue021" }, + { "garage", "\uf011" }, + { "gas_meter", "\uec19" }, + { "gavel", "\ue90e" }, + { "generating_tokens", "\ue749" }, + { "gesture", "\ue155" }, + { "get_app", "\ue884" }, + { "gif", "\ue908" }, + { "gif_box", "\ue7a3" }, + { "girl", "\ueb68" }, + { "gite", "\ue58b" }, + { "goat", "\u10fffd" }, + { "golf_course", "\ueb45" }, + { "gpp_bad", "\uf012" }, + { "gpp_good", "\uf013" }, + { "gpp_maybe", "\uf014" }, + { "gps_fixed", "\ue1b3" }, + { "gps_not_fixed", "\ue1b4" }, + { "gps_off", "\ue1b5" }, + { "grade", "\ue885" }, + { "gradient", "\ue3e9" }, + { "grading", "\uea4f" }, + { "grain", "\ue3ea" }, + { "graphic_eq", "\ue1b8" }, + { "grass", "\uf205" }, + { "grid_3x3", "\uf015" }, + { "grid_4x4", "\uf016" }, + { "grid_goldenratio", "\uf017" }, + { "grid_off", "\ue3eb" }, + { "grid_on", "\ue3ec" }, + { "grid_view", "\ue9b0" }, + { "group", "\ue7ef" }, + { "group_add", "\ue7f0" }, + { "group_off", "\ue747" }, + { "group_remove", "\ue7ad" }, + { "group_work", "\ue886" }, + { "groups", "\uf233" }, + { "groups_2", "\uf8df" }, + { "groups_3", "\uf8e0" }, + { "h_mobiledata", "\uf018" }, + { "h_plus_mobiledata", "\uf019" }, + { "hail", "\ue9b1" }, + { "handshake", "\uebcb" }, + { "handyman", "\uf10b" }, + { "hardware", "\uea59" }, + { "hd", "\ue052" }, + { "hdr_auto", "\uf01a" }, + { "hdr_auto_select", "\uf01b" }, + { "hdr_enhanced_select", "\uef51" }, + { "hdr_off", "\ue3ed" }, + { "hdr_off_select", "\uf01c" }, + { "hdr_on", "\ue3ee" }, + { "hdr_on_select", "\uf01d" }, + { "hdr_plus", "\uf01e" }, + { "hdr_strong", "\ue3f1" }, + { "hdr_weak", "\ue3f2" }, + { "headphones", "\uf01f" }, + { "headphones_battery", "\uf020" }, + { "headset", "\ue310" }, + { "headset_mic", "\ue311" }, + { "headset_off", "\ue33a" }, + { "healing", "\ue3f3" }, + { "health_and_safety", "\ue1d5" }, + { "hearing", "\ue023" }, + { "hearing_disabled", "\uf104" }, + { "heart_broken", "\ueac2" }, + { "heat_pump", "\uec18" }, + { "height", "\uea16" }, + { "help", "\ue887" }, + { "help_center", "\uf1c0" }, + { "help_outline", "\ue8fd" }, + { "hevc", "\uf021" }, + { "hexagon", "\ueb39" }, + { "hide_image", "\uf022" }, + { "hide_source", "\uf023" }, + { "high_quality", "\ue024" }, + { "highlight", "\ue25f" }, + { "highlight_alt", "\uef52" }, + { "highlight_off", "\ue888" }, + { "highlight_remove", "\ue888" }, + { "hiking", "\ue50a" }, + { "history", "\ue889" }, + { "history_edu", "\uea3e" }, + { "history_toggle_off", "\uf17d" }, + { "hive", "\ueaa6" }, + { "hls", "\ueb8a" }, + { "hls_off", "\ueb8c" }, + { "holiday_village", "\ue58a" }, + { "home", "\ue88a" }, + { "home_filled", "\ue9b2" }, + { "home_max", "\uf024" }, + { "home_mini", "\uf025" }, + { "home_repair_service", "\uf100" }, + { "home_work", "\uea09" }, + { "horizontal_distribute", "\ue014" }, + { "horizontal_rule", "\uf108" }, + { "horizontal_split", "\ue947" }, + { "hot_tub", "\ueb46" }, + { "hotel", "\ue53a" }, + { "hotel_class", "\ue743" }, + { "hourglass_bottom", "\uea5c" }, + { "hourglass_disabled", "\uef53" }, + { "hourglass_empty", "\ue88b" }, + { "hourglass_full", "\ue88c" }, + { "hourglass_top", "\uea5b" }, + { "house", "\uea44" }, + { "house_siding", "\uf202" }, + { "houseboat", "\ue584" }, + { "how_to_reg", "\ue174" }, + { "how_to_vote", "\ue175" }, + { "html", "\ueb7e" }, + { "http", "\ue902" }, + { "https", "\ue88d" }, + { "hub", "\ue9f4" }, + { "hvac", "\uf10e" }, + { "ice_skating", "\ue50b" }, + { "icecream", "\uea69" }, + { "image", "\ue3f4" }, + { "image_aspect_ratio", "\ue3f5" }, + { "image_not_supported", "\uf116" }, + { "image_search", "\ue43f" }, + { "imagesearch_roller", "\ue9b4" }, + { "import_contacts", "\ue0e0" }, + { "import_export", "\ue0c3" }, + { "important_devices", "\ue912" }, + { "inbox", "\ue156" }, + { "incomplete_circle", "\ue79b" }, + { "indeterminate_check_box", "\ue909" }, + { "info", "\ue88e" }, + { "info_outline", "\ue88f" }, + { "input", "\ue890" }, + { "insert_chart", "\ue24b" }, + { "insert_chart_outlined", "\ue26a" }, + { "insert_comment", "\ue24c" }, + { "insert_drive_file", "\ue24d" }, + { "insert_emoticon", "\ue24e" }, + { "insert_invitation", "\ue24f" }, + { "insert_link", "\ue250" }, + { "insert_page_break", "\ueaca" }, + { "insert_photo", "\ue251" }, + { "insights", "\uf092" }, + { "install_desktop", "\ueb71" }, + { "install_mobile", "\ueb72" }, + { "integration_instructions", "\uef54" }, + { "interests", "\ue7c8" }, + { "interpreter_mode", "\ue83b" }, + { "inventory", "\ue179" }, + { "inventory_2", "\ue1a1" }, + { "invert_colors", "\ue891" }, + { "invert_colors_off", "\ue0c4" }, + { "invert_colors_on", "\ue891" }, + { "ios_share", "\ue6b8" }, + { "iron", "\ue583" }, + { "iso", "\ue3f6" }, + { "javascript", "\ueb7c" }, + { "join_full", "\ueaeb" }, + { "join_inner", "\ueaf4" }, + { "join_left", "\ueaf2" }, + { "join_right", "\ueaea" }, + { "kayaking", "\ue50c" }, + { "kebab_dining", "\ue842" }, + { "key", "\ue73c" }, + { "key_off", "\ueb84" }, + { "keyboard", "\ue312" }, + { "keyboard_alt", "\uf028" }, + { "keyboard_arrow_down", "\ue313" }, + { "keyboard_arrow_left", "\ue314" }, + { "keyboard_arrow_right", "\ue315" }, + { "keyboard_arrow_up", "\ue316" }, + { "keyboard_backspace", "\ue317" }, + { "keyboard_capslock", "\ue318" }, + { "keyboard_command", "\ueae0" }, + { "keyboard_command_key", "\ueae7" }, + { "keyboard_control", "\ue5d3" }, + { "keyboard_control_key", "\ueae6" }, + { "keyboard_double_arrow_down", "\uead0" }, + { "keyboard_double_arrow_left", "\ueac3" }, + { "keyboard_double_arrow_right", "\ueac9" }, + { "keyboard_double_arrow_up", "\ueacf" }, + { "keyboard_hide", "\ue31a" }, + { "keyboard_option", "\ueadf" }, + { "keyboard_option_key", "\ueae8" }, + { "keyboard_return", "\ue31b" }, + { "keyboard_tab", "\ue31c" }, + { "keyboard_voice", "\ue31d" }, + { "king_bed", "\uea45" }, + { "kitchen", "\ueb47" }, + { "kitesurfing", "\ue50d" }, + { "label", "\ue892" }, + { "label_important", "\ue937" }, + { "label_important_outline", "\ue948" }, + { "label_off", "\ue9b6" }, + { "label_outline", "\ue893" }, + { "lan", "\ueb2f" }, + { "landscape", "\ue3f7" }, + { "landslide", "\uebd7" }, + { "language", "\ue894" }, + { "laptop", "\ue31e" }, + { "laptop_chromebook", "\ue31f" }, + { "laptop_mac", "\ue320" }, + { "laptop_windows", "\ue321" }, + { "last_page", "\ue5dd" }, + { "launch", "\ue895" }, + { "layers", "\ue53b" }, + { "layers_clear", "\ue53c" }, + { "leaderboard", "\uf20c" }, + { "leak_add", "\ue3f8" }, + { "leak_remove", "\ue3f9" }, + { "leave_bags_at_home", "\uf21b" }, + { "legend_toggle", "\uf11b" }, + { "lens", "\ue3fa" }, + { "lens_blur", "\uf029" }, + { "library_add", "\ue02e" }, + { "library_add_check", "\ue9b7" }, + { "library_books", "\ue02f" }, + { "library_music", "\ue030" }, + { "light", "\uf02a" }, + { "light_mode", "\ue518" }, + { "lightbulb", "\ue0f0" }, + { "lightbulb_circle", "\uebfe" }, + { "lightbulb_outline", "\ue90f" }, + { "line_axis", "\uea9a" }, + { "line_style", "\ue919" }, + { "line_weight", "\ue91a" }, + { "linear_scale", "\ue260" }, + { "link", "\ue157" }, + { "link_off", "\ue16f" }, + { "linked_camera", "\ue438" }, + { "liquor", "\uea60" }, + { "list", "\ue896" }, + { "list_alt", "\ue0ee" }, + { "live_help", "\ue0c6" }, + { "live_tv", "\ue639" }, + { "living", "\uf02b" }, + { "local_activity", "\ue53f" }, + { "local_airport", "\ue53d" }, + { "local_atm", "\ue53e" }, + { "local_attraction", "\ue53f" }, + { "local_bar", "\ue540" }, + { "local_cafe", "\ue541" }, + { "local_car_wash", "\ue542" }, + { "local_convenience_store", "\ue543" }, + { "local_dining", "\ue556" }, + { "local_drink", "\ue544" }, + { "local_fire_department", "\uef55" }, + { "local_florist", "\ue545" }, + { "local_gas_station", "\ue546" }, + { "local_grocery_store", "\ue547" }, + { "local_hospital", "\ue548" }, + { "local_hotel", "\ue549" }, + { "local_laundry_service", "\ue54a" }, + { "local_library", "\ue54b" }, + { "local_mall", "\ue54c" }, + { "local_movies", "\ue54d" }, + { "local_offer", "\ue54e" }, + { "local_parking", "\ue54f" }, + { "local_pharmacy", "\ue550" }, + { "local_phone", "\ue551" }, + { "local_pizza", "\ue552" }, + { "local_play", "\ue553" }, + { "local_police", "\uef56" }, + { "local_post_office", "\ue554" }, + { "local_print_shop", "\ue555" }, + { "local_printshop", "\ue555" }, + { "local_restaurant", "\ue556" }, + { "local_see", "\ue557" }, + { "local_shipping", "\ue558" }, + { "local_taxi", "\ue559" }, + { "location_city", "\ue7f1" }, + { "location_disabled", "\ue1b6" }, + { "location_history", "\ue55a" }, + { "location_off", "\ue0c7" }, + { "location_on", "\ue0c8" }, + { "location_pin", "\uf1db" }, + { "location_searching", "\ue1b7" }, + { "lock", "\ue897" }, + { "lock_clock", "\uef57" }, + { "lock_open", "\ue898" }, + { "lock_outline", "\ue899" }, + { "lock_person", "\uf8f3" }, + { "lock_reset", "\ueade" }, + { "login", "\uea77" }, + { "logo_dev", "\uead6" }, + { "logout", "\ue9ba" }, + { "looks", "\ue3fc" }, + { "looks_3", "\ue3fb" }, + { "looks_4", "\ue3fd" }, + { "looks_5", "\ue3fe" }, + { "looks_6", "\ue3ff" }, + { "looks_one", "\ue400" }, + { "looks_two", "\ue401" }, + { "loop", "\ue028" }, + { "loupe", "\ue402" }, + { "low_priority", "\ue16d" }, + { "loyalty", "\ue89a" }, + { "lte_mobiledata", "\uf02c" }, + { "lte_plus_mobiledata", "\uf02d" }, + { "luggage", "\uf235" }, + { "lunch_dining", "\uea61" }, + { "lyrics", "\uec0b" }, + { "macro_off", "\uf8d2" }, + { "mail", "\ue158" }, + { "mail_lock", "\uec0a" }, + { "mail_outline", "\ue0e1" }, + { "male", "\ue58e" }, + { "man", "\ue4eb" }, + { "man_2", "\uf8e1" }, + { "man_3", "\uf8e2" }, + { "man_4", "\uf8e3" }, + { "manage_accounts", "\uf02e" }, + { "manage_history", "\uebe7" }, + { "manage_search", "\uf02f" }, + { "map", "\ue55b" }, + { "maps_home_work", "\uf030" }, + { "maps_ugc", "\uef58" }, + { "margin", "\ue9bb" }, + { "mark_as_unread", "\ue9bc" }, + { "mark_chat_read", "\uf18b" }, + { "mark_chat_unread", "\uf189" }, + { "mark_email_read", "\uf18c" }, + { "mark_email_unread", "\uf18a" }, + { "mark_unread_chat_alt", "\ueb9d" }, + { "markunread", "\ue159" }, + { "markunread_mailbox", "\ue89b" }, + { "masks", "\uf218" }, + { "maximize", "\ue930" }, + { "media_bluetooth_off", "\uf031" }, + { "media_bluetooth_on", "\uf032" }, + { "mediation", "\uefa7" }, + { "medical_information", "\uebed" }, + { "medical_services", "\uf109" }, + { "medication", "\uf033" }, + { "medication_liquid", "\uea87" }, + { "meeting_room", "\ueb4f" }, + { "memory", "\ue322" }, + { "menu", "\ue5d2" }, + { "menu_book", "\uea19" }, + { "menu_open", "\ue9bd" }, + { "merge", "\ueb98" }, + { "merge_type", "\ue252" }, + { "message", "\ue0c9" }, + { "messenger", "\ue0ca" }, + { "messenger_outline", "\ue0cb" }, + { "mic", "\ue029" }, + { "mic_external_off", "\uef59" }, + { "mic_external_on", "\uef5a" }, + { "mic_none", "\ue02a" }, + { "mic_off", "\ue02b" }, + { "microwave", "\uf204" }, + { "military_tech", "\uea3f" }, + { "minimize", "\ue931" }, + { "minor_crash", "\uebf1" }, + { "miscellaneous_services", "\uf10c" }, + { "missed_video_call", "\ue073" }, + { "mms", "\ue618" }, + { "mobile_friendly", "\ue200" }, + { "mobile_off", "\ue201" }, + { "mobile_screen_share", "\ue0e7" }, + { "mobiledata_off", "\uf034" }, + { "mode", "\uf097" }, + { "mode_comment", "\ue253" }, + { "mode_edit", "\ue254" }, + { "mode_edit_outline", "\uf035" }, + { "mode_fan_off", "\uec17" }, + { "mode_night", "\uf036" }, + { "mode_of_travel", "\ue7ce" }, + { "mode_standby", "\uf037" }, + { "model_training", "\uf0cf" }, + { "monetization_on", "\ue263" }, + { "money", "\ue57d" }, + { "money_off", "\ue25c" }, + { "money_off_csred", "\uf038" }, + { "monitor", "\uef5b" }, + { "monitor_heart", "\ueaa2" }, + { "monitor_weight", "\uf039" }, + { "monochrome_photos", "\ue403" }, + { "mood", "\ue7f2" }, + { "mood_bad", "\ue7f3" }, + { "moped", "\ueb28" }, + { "more", "\ue619" }, + { "more_horiz", "\ue5d3" }, + { "more_time", "\uea5d" }, + { "more_vert", "\ue5d4" }, + { "mosque", "\ueab2" }, + { "motion_photos_auto", "\uf03a" }, + { "motion_photos_off", "\ue9c0" }, + { "motion_photos_on", "\ue9c1" }, + { "motion_photos_pause", "\uf227" }, + { "motion_photos_paused", "\ue9c2" }, + { "motorcycle", "\ue91b" }, + { "mouse", "\ue323" }, + { "move_down", "\ueb61" }, + { "move_to_inbox", "\ue168" }, + { "move_up", "\ueb64" }, + { "movie", "\ue02c" }, + { "movie_creation", "\ue404" }, + { "movie_edit", "\uf840" }, + { "movie_filter", "\ue43a" }, + { "moving", "\ue501" }, + { "mp", "\ue9c3" }, + { "multiline_chart", "\ue6df" }, + { "multiple_stop", "\uf1b9" }, + { "multitrack_audio", "\ue1b8" }, + { "museum", "\uea36" }, + { "music_note", "\ue405" }, + { "music_off", "\ue440" }, + { "music_video", "\ue063" }, + { "my_library_add", "\ue02e" }, + { "my_library_books", "\ue02f" }, + { "my_library_music", "\ue030" }, + { "my_location", "\ue55c" }, + { "nat", "\uef5c" }, + { "nature", "\ue406" }, + { "nature_people", "\ue407" }, + { "navigate_before", "\ue408" }, + { "navigate_next", "\ue409" }, + { "navigation", "\ue55d" }, + { "near_me", "\ue569" }, + { "near_me_disabled", "\uf1ef" }, + { "nearby_error", "\uf03b" }, + { "nearby_off", "\uf03c" }, + { "nest_cam_wired_stand", "\uec16" }, + { "network_cell", "\ue1b9" }, + { "network_check", "\ue640" }, + { "network_locked", "\ue61a" }, + { "network_ping", "\uebca" }, + { "network_wifi", "\ue1ba" }, + { "network_wifi_1_bar", "\uebe4" }, + { "network_wifi_2_bar", "\uebd6" }, + { "network_wifi_3_bar", "\uebe1" }, + { "new_label", "\ue609" }, + { "new_releases", "\ue031" }, + { "newspaper", "\ueb81" }, + { "next_plan", "\uef5d" }, + { "next_week", "\ue16a" }, + { "nfc", "\ue1bb" }, + { "night_shelter", "\uf1f1" }, + { "nightlife", "\uea62" }, + { "nightlight", "\uf03d" }, + { "nightlight_round", "\uef5e" }, + { "nights_stay", "\uea46" }, + { "no_accounts", "\uf03e" }, + { "no_adult_content", "\uf8fe" }, + { "no_backpack", "\uf237" }, + { "no_cell", "\uf1a4" }, + { "no_crash", "\uebf0" }, + { "no_drinks", "\uf1a5" }, + { "no_encryption", "\ue641" }, + { "no_encryption_gmailerrorred", "\uf03f" }, + { "no_flash", "\uf1a6" }, + { "no_food", "\uf1a7" }, + { "no_luggage", "\uf23b" }, + { "no_meals", "\uf1d6" }, + { "no_meals_ouline", "\uf229" }, + { "no_meeting_room", "\ueb4e" }, + { "no_photography", "\uf1a8" }, + { "no_sim", "\ue0cc" }, + { "no_stroller", "\uf1af" }, + { "no_transfer", "\uf1d5" }, + { "noise_aware", "\uebec" }, + { "noise_control_off", "\uebf3" }, + { "nordic_walking", "\ue50e" }, + { "north", "\uf1e0" }, + { "north_east", "\uf1e1" }, + { "north_west", "\uf1e2" }, + { "not_accessible", "\uf0fe" }, + { "not_interested", "\ue033" }, + { "not_listed_location", "\ue575" }, + { "not_started", "\uf0d1" }, + { "note", "\ue06f" }, + { "note_add", "\ue89c" }, + { "note_alt", "\uf040" }, + { "notes", "\ue26c" }, + { "notification_add", "\ue399" }, + { "notification_important", "\ue004" }, + { "notifications", "\ue7f4" }, + { "notifications_active", "\ue7f7" }, + { "notifications_none", "\ue7f5" }, + { "notifications_off", "\ue7f6" }, + { "notifications_on", "\ue7f7" }, + { "notifications_paused", "\ue7f8" }, + { "now_wallpaper", "\ue1bc" }, + { "now_widgets", "\ue1bd" }, + { "numbers", "\ueac7" }, + { "offline_bolt", "\ue932" }, + { "offline_pin", "\ue90a" }, + { "offline_share", "\ue9c5" }, + { "oil_barrel", "\uec15" }, + { "on_device_training", "\uebfd" }, + { "ondemand_video", "\ue63a" }, + { "online_prediction", "\uf0eb" }, + { "opacity", "\ue91c" }, + { "open_in_browser", "\ue89d" }, + { "open_in_full", "\uf1ce" }, + { "open_in_new", "\ue89e" }, + { "open_in_new_off", "\ue4f6" }, + { "open_with", "\ue89f" }, + { "other_houses", "\ue58c" }, + { "outbond", "\uf228" }, + { "outbound", "\ue1ca" }, + { "outbox", "\uef5f" }, + { "outdoor_grill", "\uea47" }, + { "outgoing_mail", "\uf0d2" }, + { "outlet", "\uf1d4" }, + { "outlined_flag", "\ue16e" }, + { "output", "\uebbe" }, + { "padding", "\ue9c8" }, + { "pages", "\ue7f9" }, + { "pageview", "\ue8a0" }, + { "paid", "\uf041" }, + { "palette", "\ue40a" }, + { "pallet", "\uf86a" }, + { "pan_tool", "\ue925" }, + { "pan_tool_alt", "\uebb9" }, + { "panorama", "\ue40b" }, + { "panorama_fish_eye", "\ue40c" }, + { "panorama_fisheye", "\ue40c" }, + { "panorama_horizontal", "\ue40d" }, + { "panorama_horizontal_select", "\uef60" }, + { "panorama_photosphere", "\ue9c9" }, + { "panorama_photosphere_select", "\ue9ca" }, + { "panorama_vertical", "\ue40e" }, + { "panorama_vertical_select", "\uef61" }, + { "panorama_wide_angle", "\ue40f" }, + { "panorama_wide_angle_select", "\uef62" }, + { "paragliding", "\ue50f" }, + { "park", "\uea63" }, + { "party_mode", "\ue7fa" }, + { "password", "\uf042" }, + { "paste", "\uf098" }, + { "pattern", "\uf043" }, + { "pause", "\ue034" }, + { "pause_circle", "\ue1a2" }, + { "pause_circle_filled", "\ue035" }, + { "pause_circle_outline", "\ue036" }, + { "pause_presentation", "\ue0ea" }, + { "payment", "\ue8a1" }, + { "payments", "\uef63" }, + { "paypal", "\uea8d" }, + { "pedal_bike", "\ueb29" }, + { "pending", "\uef64" }, + { "pending_actions", "\uf1bb" }, + { "pentagon", "\ueb50" }, + { "people", "\ue7fb" }, + { "people_alt", "\uea21" }, + { "people_outline", "\ue7fc" }, + { "percent", "\ueb58" }, + { "perm_camera_mic", "\ue8a2" }, + { "perm_contact_cal", "\ue8a3" }, + { "perm_contact_calendar", "\ue8a3" }, + { "perm_data_setting", "\ue8a4" }, + { "perm_device_info", "\ue8a5" }, + { "perm_device_information", "\ue8a5" }, + { "perm_identity", "\ue8a6" }, + { "perm_media", "\ue8a7" }, + { "perm_phone_msg", "\ue8a8" }, + { "perm_scan_wifi", "\ue8a9" }, + { "person", "\ue7fd" }, + { "person_2", "\uf8e4" }, + { "person_3", "\uf8e5" }, + { "person_4", "\uf8e6" }, + { "person_add", "\ue7fe" }, + { "person_add_alt", "\uea4d" }, + { "person_add_alt_1", "\uef65" }, + { "person_add_disabled", "\ue9cb" }, + { "person_off", "\ue510" }, + { "person_outline", "\ue7ff" }, + { "person_pin", "\ue55a" }, + { "person_pin_circle", "\ue56a" }, + { "person_remove", "\uef66" }, + { "person_remove_alt_1", "\uef67" }, + { "person_search", "\uf106" }, + { "personal_injury", "\ue6da" }, + { "personal_video", "\ue63b" }, + { "pest_control", "\uf0fa" }, + { "pest_control_rodent", "\uf0fd" }, + { "pets", "\ue91d" }, + { "phishing", "\uead7" }, + { "phone", "\ue0cd" }, + { "phone_android", "\ue324" }, + { "phone_bluetooth_speaker", "\ue61b" }, + { "phone_callback", "\ue649" }, + { "phone_disabled", "\ue9cc" }, + { "phone_enabled", "\ue9cd" }, + { "phone_forwarded", "\ue61c" }, + { "phone_in_talk", "\ue61d" }, + { "phone_iphone", "\ue325" }, + { "phone_locked", "\ue61e" }, + { "phone_missed", "\ue61f" }, + { "phone_paused", "\ue620" }, + { "phonelink", "\ue326" }, + { "phonelink_erase", "\ue0db" }, + { "phonelink_lock", "\ue0dc" }, + { "phonelink_off", "\ue327" }, + { "phonelink_ring", "\ue0dd" }, + { "phonelink_setup", "\ue0de" }, + { "photo", "\ue410" }, + { "photo_album", "\ue411" }, + { "photo_camera", "\ue412" }, + { "photo_camera_back", "\uef68" }, + { "photo_camera_front", "\uef69" }, + { "photo_filter", "\ue43b" }, + { "photo_library", "\ue413" }, + { "photo_size_select_actual", "\ue432" }, + { "photo_size_select_large", "\ue433" }, + { "photo_size_select_small", "\ue434" }, + { "php", "\ueb8f" }, + { "piano", "\ue521" }, + { "piano_off", "\ue520" }, + { "picture_as_pdf", "\ue415" }, + { "picture_in_picture", "\ue8aa" }, + { "picture_in_picture_alt", "\ue911" }, + { "pie_chart", "\ue6c4" }, + { "pie_chart_outline", "\uf044" }, + { "pie_chart_outlined", "\ue6c5" }, + { "pin", "\uf045" }, + { "pin_drop", "\ue55e" }, + { "pin_end", "\ue767" }, + { "pin_invoke", "\ue763" }, + { "pinch", "\ueb38" }, + { "pivot_table_chart", "\ue9ce" }, + { "pix", "\ueaa3" }, + { "place", "\ue55f" }, + { "plagiarism", "\uea5a" }, + { "play_arrow", "\ue037" }, + { "play_circle", "\ue1c4" }, + { "play_circle_fill", "\ue038" }, + { "play_circle_filled", "\ue038" }, + { "play_circle_outline", "\ue039" }, + { "play_disabled", "\uef6a" }, + { "play_for_work", "\ue906" }, + { "play_lesson", "\uf047" }, + { "playlist_add", "\ue03b" }, + { "playlist_add_check", "\ue065" }, + { "playlist_add_check_circle", "\ue7e6" }, + { "playlist_add_circle", "\ue7e5" }, + { "playlist_play", "\ue05f" }, + { "playlist_remove", "\ueb80" }, + { "plumbing", "\uf107" }, + { "plus_one", "\ue800" }, + { "podcasts", "\uf048" }, + { "point_of_sale", "\uf17e" }, + { "policy", "\uea17" }, + { "poll", "\ue801" }, + { "polyline", "\uebbb" }, + { "polymer", "\ue8ab" }, + { "pool", "\ueb48" }, + { "portable_wifi_off", "\ue0ce" }, + { "portrait", "\ue416" }, + { "post_add", "\uea20" }, + { "power", "\ue63c" }, + { "power_input", "\ue336" }, + { "power_off", "\ue646" }, + { "power_settings_new", "\ue8ac" }, + { "precision_manufacturing", "\uf049" }, + { "pregnant_woman", "\ue91e" }, + { "present_to_all", "\ue0df" }, + { "preview", "\uf1c5" }, + { "price_change", "\uf04a" }, + { "price_check", "\uf04b" }, + { "print", "\ue8ad" }, + { "print_disabled", "\ue9cf" }, + { "priority_high", "\ue645" }, + { "privacy_tip", "\uf0dc" }, + { "private_connectivity", "\ue744" }, + { "production_quantity_limits", "\ue1d1" }, + { "propane", "\uec14" }, + { "propane_tank", "\uec13" }, + { "psychology", "\uea4a" }, + { "psychology_alt", "\uf8ea" }, + { "public", "\ue80b" }, + { "public_off", "\uf1ca" }, + { "publish", "\ue255" }, + { "published_with_changes", "\uf232" }, + { "punch_clock", "\ueaa8" }, + { "push_pin", "\uf10d" }, + { "qr_code", "\uef6b" }, + { "qr_code_2", "\ue00a" }, + { "qr_code_scanner", "\uf206" }, + { "query_builder", "\ue8ae" }, + { "query_stats", "\ue4fc" }, + { "question_answer", "\ue8af" }, + { "question_mark", "\ueb8b" }, + { "queue", "\ue03c" }, + { "queue_music", "\ue03d" }, + { "queue_play_next", "\ue066" }, + { "quick_contacts_dialer", "\ue0cf" }, + { "quick_contacts_mail", "\ue0d0" }, + { "quickreply", "\uef6c" }, + { "quiz", "\uf04c" }, + { "quora", "\uea98" }, + { "r_mobiledata", "\uf04d" }, + { "radar", "\uf04e" }, + { "radio", "\ue03e" }, + { "radio_button_checked", "\ue837" }, + { "radio_button_off", "\ue836" }, + { "radio_button_on", "\ue837" }, + { "radio_button_unchecked", "\ue836" }, + { "railway_alert", "\ue9d1" }, + { "ramen_dining", "\uea64" }, + { "ramp_left", "\ueb9c" }, + { "ramp_right", "\ueb96" }, + { "rate_review", "\ue560" }, + { "raw_off", "\uf04f" }, + { "raw_on", "\uf050" }, + { "read_more", "\uef6d" }, + { "real_estate_agent", "\ue73a" }, + { "rebase_edit", "\uf846" }, + { "receipt", "\ue8b0" }, + { "receipt_long", "\uef6e" }, + { "recent_actors", "\ue03f" }, + { "recommend", "\ue9d2" }, + { "record_voice_over", "\ue91f" }, + { "rectangle", "\ueb54" }, + { "recycling", "\ue760" }, + { "reddit", "\ueaa0" }, + { "redeem", "\ue8b1" }, + { "redo", "\ue15a" }, + { "reduce_capacity", "\uf21c" }, + { "refresh", "\ue5d5" }, + { "remember_me", "\uf051" }, + { "remove", "\ue15b" }, + { "remove_circle", "\ue15c" }, + { "remove_circle_outline", "\ue15d" }, + { "remove_done", "\ue9d3" }, + { "remove_from_queue", "\ue067" }, + { "remove_moderator", "\ue9d4" }, + { "remove_red_eye", "\ue417" }, + { "remove_road", "\uebfc" }, + { "remove_shopping_cart", "\ue928" }, + { "reorder", "\ue8fe" }, + { "repartition", "\uf8e8" }, + { "repeat", "\ue040" }, + { "repeat_on", "\ue9d6" }, + { "repeat_one", "\ue041" }, + { "repeat_one_on", "\ue9d7" }, + { "replay", "\ue042" }, + { "replay_10", "\ue059" }, + { "replay_30", "\ue05a" }, + { "replay_5", "\ue05b" }, + { "replay_circle_filled", "\ue9d8" }, + { "reply", "\ue15e" }, + { "reply_all", "\ue15f" }, + { "report", "\ue160" }, + { "report_gmailerrorred", "\uf052" }, + { "report_off", "\ue170" }, + { "report_problem", "\ue8b2" }, + { "request_page", "\uf22c" }, + { "request_quote", "\uf1b6" }, + { "reset_tv", "\ue9d9" }, + { "restart_alt", "\uf053" }, + { "restaurant", "\ue56c" }, + { "restaurant_menu", "\ue561" }, + { "restore", "\ue8b3" }, + { "restore_from_trash", "\ue938" }, + { "restore_page", "\ue929" }, + { "reviews", "\uf054" }, + { "rice_bowl", "\uf1f5" }, + { "ring_volume", "\ue0d1" }, + { "rocket", "\ueba5" }, + { "rocket_launch", "\ueb9b" }, + { "roller_shades", "\uec12" }, + { "roller_shades_closed", "\uec11" }, + { "roller_skating", "\uebcd" }, + { "roofing", "\uf201" }, + { "room", "\ue8b4" }, + { "room_preferences", "\uf1b8" }, + { "room_service", "\ueb49" }, + { "rotate_90_degrees_ccw", "\ue418" }, + { "rotate_90_degrees_cw", "\ueaab" }, + { "rotate_left", "\ue419" }, + { "rotate_right", "\ue41a" }, + { "roundabout_left", "\ueb99" }, + { "roundabout_right", "\ueba3" }, + { "rounded_corner", "\ue920" }, + { "route", "\ueacd" }, + { "router", "\ue328" }, + { "rowing", "\ue921" }, + { "rss_feed", "\ue0e5" }, + { "rsvp", "\uf055" }, + { "rtt", "\ue9ad" }, + { "rule", "\uf1c2" }, + { "rule_folder", "\uf1c9" }, + { "run_circle", "\uef6f" }, + { "running_with_errors", "\ue51d" }, + { "rv_hookup", "\ue642" }, + { "safety_check", "\uebef" }, + { "safety_divider", "\ue1cc" }, + { "sailing", "\ue502" }, + { "sanitizer", "\uf21d" }, + { "satellite", "\ue562" }, + { "satellite_alt", "\ueb3a" }, + { "save", "\ue161" }, + { "save_alt", "\ue171" }, + { "save_as", "\ueb60" }, + { "saved_search", "\uea11" }, + { "savings", "\ue2eb" }, + { "scale", "\ueb5f" }, + { "scanner", "\ue329" }, + { "scatter_plot", "\ue268" }, + { "schedule", "\ue8b5" }, + { "schedule_send", "\uea0a" }, + { "schema", "\ue4fd" }, + { "school", "\ue80c" }, + { "science", "\uea4b" }, + { "score", "\ue269" }, + { "scoreboard", "\uebd0" }, + { "screen_lock_landscape", "\ue1be" }, + { "screen_lock_portrait", "\ue1bf" }, + { "screen_lock_rotation", "\ue1c0" }, + { "screen_rotation", "\ue1c1" }, + { "screen_rotation_alt", "\uebee" }, + { "screen_search_desktop", "\uef70" }, + { "screen_share", "\ue0e2" }, + { "screenshot", "\uf056" }, + { "screenshot_monitor", "\uec08" }, + { "scuba_diving", "\uebce" }, + { "sd", "\ue9dd" }, + { "sd_card", "\ue623" }, + { "sd_card_alert", "\uf057" }, + { "sd_storage", "\ue1c2" }, + { "search", "\ue8b6" }, + { "search_off", "\uea76" }, + { "security", "\ue32a" }, + { "security_update", "\uf058" }, + { "security_update_good", "\uf059" }, + { "security_update_warning", "\uf05a" }, + { "segment", "\ue94b" }, + { "select_all", "\ue162" }, + { "self_improvement", "\uea78" }, + { "sell", "\uf05b" }, + { "send", "\ue163" }, + { "send_and_archive", "\uea0c" }, + { "send_time_extension", "\ueadb" }, + { "send_to_mobile", "\uf05c" }, + { "sensor_door", "\uf1b5" }, + { "sensor_occupied", "\uec10" }, + { "sensor_window", "\uf1b4" }, + { "sensors", "\ue51e" }, + { "sensors_off", "\ue51f" }, + { "sentiment_dissatisfied", "\ue811" }, + { "sentiment_neutral", "\ue812" }, + { "sentiment_satisfied", "\ue813" }, + { "sentiment_satisfied_alt", "\ue0ed" }, + { "sentiment_very_dissatisfied", "\ue814" }, + { "sentiment_very_satisfied", "\ue815" }, + { "set_meal", "\uf1ea" }, + { "settings", "\ue8b8" }, + { "settings_accessibility", "\uf05d" }, + { "settings_applications", "\ue8b9" }, + { "settings_backup_restore", "\ue8ba" }, + { "settings_bluetooth", "\ue8bb" }, + { "settings_brightness", "\ue8bd" }, + { "settings_cell", "\ue8bc" }, + { "settings_display", "\ue8bd" }, + { "settings_ethernet", "\ue8be" }, + { "settings_input_antenna", "\ue8bf" }, + { "settings_input_component", "\ue8c0" }, + { "settings_input_composite", "\ue8c1" }, + { "settings_input_hdmi", "\ue8c2" }, + { "settings_input_svideo", "\ue8c3" }, + { "settings_overscan", "\ue8c4" }, + { "settings_phone", "\ue8c5" }, + { "settings_power", "\ue8c6" }, + { "settings_remote", "\ue8c7" }, + { "settings_suggest", "\uf05e" }, + { "settings_system_daydream", "\ue1c3" }, + { "settings_voice", "\ue8c8" }, + { "severe_cold", "\uebd3" }, + { "shape_line", "\uf8d3" }, + { "share", "\ue80d" }, + { "share_arrival_time", "\ue524" }, + { "share_location", "\uf05f" }, + { "shelves", "\uf86e" }, + { "shield", "\ue9e0" }, + { "shield_moon", "\ueaa9" }, + { "shop", "\ue8c9" }, + { "shop_2", "\ue19e" }, + { "shop_two", "\ue8ca" }, + { "shopify", "\uea9d" }, + { "shopping_bag", "\uf1cc" }, + { "shopping_basket", "\ue8cb" }, + { "shopping_cart", "\ue8cc" }, + { "shopping_cart_checkout", "\ueb88" }, + { "short_text", "\ue261" }, + { "shortcut", "\uf060" }, + { "show_chart", "\ue6e1" }, + { "shower", "\uf061" }, + { "shuffle", "\ue043" }, + { "shuffle_on", "\ue9e1" }, + { "shutter_speed", "\ue43d" }, + { "sick", "\uf220" }, + { "sign_language", "\uebe5" }, + { "signal_cellular_0_bar", "\uf0a8" }, + { "signal_cellular_1_bar", "\uf0a9" }, + { "signal_cellular_2_bar", "\uf0aa" }, + { "signal_cellular_3_bar", "\uf0ab" }, + { "signal_cellular_4_bar", "\ue1c8" }, + { "signal_cellular_alt", "\ue202" }, + { "signal_cellular_alt_1_bar", "\uebdf" }, + { "signal_cellular_alt_2_bar", "\uebe3" }, + { "signal_cellular_connected_no_internet_0_bar", "\uf0ac" }, + { "signal_cellular_connected_no_internet_1_bar", "\uf0ad" }, + { "signal_cellular_connected_no_internet_2_bar", "\uf0ae" }, + { "signal_cellular_connected_no_internet_3_bar", "\uf0af" }, + { "signal_cellular_connected_no_internet_4_bar", "\ue1cd" }, + { "signal_cellular_no_sim", "\ue1ce" }, + { "signal_cellular_nodata", "\uf062" }, + { "signal_cellular_null", "\ue1cf" }, + { "signal_cellular_off", "\ue1d0" }, + { "signal_wifi_0_bar", "\uf0b0" }, + { "signal_wifi_1_bar", "\uf0b1" }, + { "signal_wifi_1_bar_lock", "\uf0b2" }, + { "signal_wifi_2_bar", "\uf0b3" }, + { "signal_wifi_2_bar_lock", "\uf0b4" }, + { "signal_wifi_3_bar", "\uf0b5" }, + { "signal_wifi_3_bar_lock", "\uf0b6" }, + { "signal_wifi_4_bar", "\ue1d8" }, + { "signal_wifi_4_bar_lock", "\ue1d9" }, + { "signal_wifi_bad", "\uf063" }, + { "signal_wifi_connected_no_internet_0", "\uf0f2" }, + { "signal_wifi_connected_no_internet_1", "\uf0ee" }, + { "signal_wifi_connected_no_internet_2", "\uf0f1" }, + { "signal_wifi_connected_no_internet_3", "\uf0ed" }, + { "signal_wifi_connected_no_internet_4", "\uf064" }, + { "signal_wifi_off", "\ue1da" }, + { "signal_wifi_statusbar_1_bar", "\uf0e6" }, + { "signal_wifi_statusbar_2_bar", "\uf0f0" }, + { "signal_wifi_statusbar_3_bar", "\uf0ea" }, + { "signal_wifi_statusbar_4_bar", "\uf065" }, + { "signal_wifi_statusbar_connected_no_internet", "\uf0f8" }, + { "signal_wifi_statusbar_connected_no_internet_1", "\uf0e9" }, + { "signal_wifi_statusbar_connected_no_internet_2", "\uf0f7" }, + { "signal_wifi_statusbar_connected_no_internet_3", "\uf0e8" }, + { "signal_wifi_statusbar_connected_no_internet_4", "\uf066" }, + { "signal_wifi_statusbar_not_connected", "\uf0ef" }, + { "signal_wifi_statusbar_null", "\uf067" }, + { "signpost", "\ueb91" }, + { "sim_card", "\ue32b" }, + { "sim_card_alert", "\ue624" }, + { "sim_card_download", "\uf068" }, + { "single_bed", "\uea48" }, + { "sip", "\uf069" }, + { "skateboarding", "\ue511" }, + { "skip_next", "\ue044" }, + { "skip_previous", "\ue045" }, + { "sledding", "\ue512" }, + { "slideshow", "\ue41b" }, + { "slow_motion_video", "\ue068" }, + { "smart_button", "\uf1c1" }, + { "smart_display", "\uf06a" }, + { "smart_screen", "\uf06b" }, + { "smart_toy", "\uf06c" }, + { "smartphone", "\ue32c" }, + { "smoke_free", "\ueb4a" }, + { "smoking_rooms", "\ueb4b" }, + { "sms", "\ue625" }, + { "sms_failed", "\ue626" }, + { "snapchat", "\uea6e" }, + { "snippet_folder", "\uf1c7" }, + { "snooze", "\ue046" }, + { "snowboarding", "\ue513" }, + { "snowing", "\ue80f" }, + { "snowmobile", "\ue503" }, + { "snowshoeing", "\ue514" }, + { "soap", "\uf1b2" }, + { "social_distance", "\ue1cb" }, + { "solar_power", "\uec0f" }, + { "sort", "\ue164" }, + { "sort_by_alpha", "\ue053" }, + { "sos", "\uebf7" }, + { "soup_kitchen", "\ue7d3" }, + { "source", "\uf1c4" }, + { "south", "\uf1e3" }, + { "south_america", "\ue7e4" }, + { "south_east", "\uf1e4" }, + { "south_west", "\uf1e5" }, + { "spa", "\ueb4c" }, + { "space_bar", "\ue256" }, + { "space_dashboard", "\ue66b" }, + { "spatial_audio", "\uebeb" }, + { "spatial_audio_off", "\uebe8" }, + { "spatial_tracking", "\uebea" }, + { "speaker", "\ue32d" }, + { "speaker_group", "\ue32e" }, + { "speaker_notes", "\ue8cd" }, + { "speaker_notes_off", "\ue92a" }, + { "speaker_phone", "\ue0d2" }, + { "speed", "\ue9e4" }, + { "spellcheck", "\ue8ce" }, + { "splitscreen", "\uf06d" }, + { "spoke", "\ue9a7" }, + { "sports", "\uea30" }, + { "sports_bar", "\uf1f3" }, + { "sports_baseball", "\uea51" }, + { "sports_basketball", "\uea26" }, + { "sports_cricket", "\uea27" }, + { "sports_esports", "\uea28" }, + { "sports_football", "\uea29" }, + { "sports_golf", "\uea2a" }, + { "sports_gymnastics", "\uebc4" }, + { "sports_handball", "\uea33" }, + { "sports_hockey", "\uea2b" }, + { "sports_kabaddi", "\uea34" }, + { "sports_martial_arts", "\ueae9" }, + { "sports_mma", "\uea2c" }, + { "sports_motorsports", "\uea2d" }, + { "sports_rugby", "\uea2e" }, + { "sports_score", "\uf06e" }, + { "sports_soccer", "\uea2f" }, + { "sports_tennis", "\uea32" }, + { "sports_volleyball", "\uea31" }, + { "square", "\ueb36" }, + { "square_foot", "\uea49" }, + { "ssid_chart", "\ueb66" }, + { "stacked_bar_chart", "\ue9e6" }, + { "stacked_line_chart", "\uf22b" }, + { "stadium", "\ueb90" }, + { "stairs", "\uf1a9" }, + { "star", "\ue838" }, + { "star_border", "\ue83a" }, + { "star_border_purple500", "\uf099" }, + { "star_half", "\ue839" }, + { "star_outline", "\uf06f" }, + { "star_purple500", "\uf09a" }, + { "star_rate", "\uf0ec" }, + { "stars", "\ue8d0" }, + { "start", "\ue089" }, + { "stay_current_landscape", "\ue0d3" }, + { "stay_current_portrait", "\ue0d4" }, + { "stay_primary_landscape", "\ue0d5" }, + { "stay_primary_portrait", "\ue0d6" }, + { "sticky_note_2", "\uf1fc" }, + { "stop", "\ue047" }, + { "stop_circle", "\uef71" }, + { "stop_screen_share", "\ue0e3" }, + { "storage", "\ue1db" }, + { "store", "\ue8d1" }, + { "store_mall_directory", "\ue563" }, + { "storefront", "\uea12" }, + { "storm", "\uf070" }, + { "straight", "\ueb95" }, + { "straighten", "\ue41c" }, + { "stream", "\ue9e9" }, + { "streetview", "\ue56e" }, + { "strikethrough_s", "\ue257" }, + { "stroller", "\uf1ae" }, + { "style", "\ue41d" }, + { "subdirectory_arrow_left", "\ue5d9" }, + { "subdirectory_arrow_right", "\ue5da" }, + { "subject", "\ue8d2" }, + { "subscript", "\uf111" }, + { "subscriptions", "\ue064" }, + { "subtitles", "\ue048" }, + { "subtitles_off", "\uef72" }, + { "subway", "\ue56f" }, + { "summarize", "\uf071" }, + { "sunny", "\ue81a" }, + { "sunny_snowing", "\ue819" }, + { "superscript", "\uf112" }, + { "supervised_user_circle", "\ue939" }, + { "supervisor_account", "\ue8d3" }, + { "support", "\uef73" }, + { "support_agent", "\uf0e2" }, + { "surfing", "\ue515" }, + { "surround_sound", "\ue049" }, + { "swap_calls", "\ue0d7" }, + { "swap_horiz", "\ue8d4" }, + { "swap_horizontal_circle", "\ue933" }, + { "swap_vert", "\ue8d5" }, + { "swap_vert_circle", "\ue8d6" }, + { "swap_vertical_circle", "\ue8d6" }, + { "swipe", "\ue9ec" }, + { "swipe_down", "\ueb53" }, + { "swipe_down_alt", "\ueb30" }, + { "swipe_left", "\ueb59" }, + { "swipe_left_alt", "\ueb33" }, + { "swipe_right", "\ueb52" }, + { "swipe_right_alt", "\ueb56" }, + { "swipe_up", "\ueb2e" }, + { "swipe_up_alt", "\ueb35" }, + { "swipe_vertical", "\ueb51" }, + { "switch_access_shortcut", "\ue7e1" }, + { "switch_access_shortcut_add", "\ue7e2" }, + { "switch_account", "\ue9ed" }, + { "switch_camera", "\ue41e" }, + { "switch_left", "\uf1d1" }, + { "switch_right", "\uf1d2" }, + { "switch_video", "\ue41f" }, + { "synagogue", "\ueab0" }, + { "sync", "\ue627" }, + { "sync_alt", "\uea18" }, + { "sync_disabled", "\ue628" }, + { "sync_lock", "\ueaee" }, + { "sync_problem", "\ue629" }, + { "system_security_update", "\uf072" }, + { "system_security_update_good", "\uf073" }, + { "system_security_update_warning", "\uf074" }, + { "system_update", "\ue62a" }, + { "system_update_alt", "\ue8d7" }, + { "system_update_tv", "\ue8d7" }, + { "tab", "\ue8d8" }, + { "tab_unselected", "\ue8d9" }, + { "table_bar", "\uead2" }, + { "table_chart", "\ue265" }, + { "table_restaurant", "\ueac6" }, + { "table_rows", "\uf101" }, + { "table_view", "\uf1be" }, + { "tablet", "\ue32f" }, + { "tablet_android", "\ue330" }, + { "tablet_mac", "\ue331" }, + { "tag", "\ue9ef" }, + { "tag_faces", "\ue420" }, + { "takeout_dining", "\uea74" }, + { "tap_and_play", "\ue62b" }, + { "tapas", "\uf1e9" }, + { "task", "\uf075" }, + { "task_alt", "\ue2e6" }, + { "taxi_alert", "\uef74" }, + { "telegram", "\uea6b" }, + { "temple_buddhist", "\ueab3" }, + { "temple_hindu", "\ueaaf" }, + { "terminal", "\ueb8e" }, + { "terrain", "\ue564" }, + { "text_decrease", "\ueadd" }, + { "text_fields", "\ue262" }, + { "text_format", "\ue165" }, + { "text_increase", "\ueae2" }, + { "text_rotate_up", "\ue93a" }, + { "text_rotate_vertical", "\ue93b" }, + { "text_rotation_angledown", "\ue93c" }, + { "text_rotation_angleup", "\ue93d" }, + { "text_rotation_down", "\ue93e" }, + { "text_rotation_none", "\ue93f" }, + { "text_snippet", "\uf1c6" }, + { "textsms", "\ue0d8" }, + { "texture", "\ue421" }, + { "theater_comedy", "\uea66" }, + { "theaters", "\ue8da" }, + { "thermostat", "\uf076" }, + { "thermostat_auto", "\uf077" }, + { "thumb_down", "\ue8db" }, + { "thumb_down_alt", "\ue816" }, + { "thumb_down_off_alt", "\ue9f2" }, + { "thumb_up", "\ue8dc" }, + { "thumb_up_alt", "\ue817" }, + { "thumb_up_off_alt", "\ue9f3" }, + { "thumbs_up_down", "\ue8dd" }, + { "thunderstorm", "\uebdb" }, + { "tiktok", "\uea7e" }, + { "time_to_leave", "\ue62c" }, + { "timelapse", "\ue422" }, + { "timeline", "\ue922" }, + { "timer", "\ue425" }, + { "timer_10", "\ue423" }, + { "timer_10_select", "\uf07a" }, + { "timer_3", "\ue424" }, + { "timer_3_select", "\uf07b" }, + { "timer_off", "\ue426" }, + { "tips_and_updates", "\ue79a" }, + { "tire_repair", "\uebc8" }, + { "title", "\ue264" }, + { "toc", "\ue8de" }, + { "today", "\ue8df" }, + { "toggle_off", "\ue9f5" }, + { "toggle_on", "\ue9f6" }, + { "token", "\uea25" }, + { "toll", "\ue8e0" }, + { "tonality", "\ue427" }, + { "topic", "\uf1c8" }, + { "tornado", "\ue199" }, + { "touch_app", "\ue913" }, + { "tour", "\uef75" }, + { "toys", "\ue332" }, + { "track_changes", "\ue8e1" }, + { "traffic", "\ue565" }, + { "train", "\ue570" }, + { "tram", "\ue571" }, + { "transcribe", "\uf8ec" }, + { "transfer_within_a_station", "\ue572" }, + { "transform", "\ue428" }, + { "transgender", "\ue58d" }, + { "transit_enterexit", "\ue579" }, + { "translate", "\ue8e2" }, + { "travel_explore", "\ue2db" }, + { "trending_down", "\ue8e3" }, + { "trending_flat", "\ue8e4" }, + { "trending_neutral", "\ue8e4" }, + { "trending_up", "\ue8e5" }, + { "trip_origin", "\ue57b" }, + { "trolley", "\uf86b" }, + { "troubleshoot", "\ue1d2" }, + { "try", "\uf07c" }, + { "tsunami", "\uebd8" }, + { "tty", "\uf1aa" }, + { "tune", "\ue429" }, + { "tungsten", "\uf07d" }, + { "turn_left", "\ueba6" }, + { "turn_right", "\uebab" }, + { "turn_sharp_left", "\ueba7" }, + { "turn_sharp_right", "\uebaa" }, + { "turn_slight_left", "\ueba4" }, + { "turn_slight_right", "\ueb9a" }, + { "turned_in", "\ue8e6" }, + { "turned_in_not", "\ue8e7" }, + { "tv", "\ue333" }, + { "tv_off", "\ue647" }, + { "two_wheeler", "\ue9f9" }, + { "type_specimen", "\uf8f0" }, + { "u_turn_left", "\ueba1" }, + { "u_turn_right", "\ueba2" }, + { "umbrella", "\uf1ad" }, + { "unarchive", "\ue169" }, + { "undo", "\ue166" }, + { "unfold_less", "\ue5d6" }, + { "unfold_less_double", "\uf8cf" }, + { "unfold_more", "\ue5d7" }, + { "unfold_more_double", "\uf8d0" }, + { "unpublished", "\uf236" }, + { "unsubscribe", "\ue0eb" }, + { "upcoming", "\uf07e" }, + { "update", "\ue923" }, + { "update_disabled", "\ue075" }, + { "upgrade", "\uf0fb" }, + { "upload", "\uf09b" }, + { "upload_file", "\ue9fc" }, + { "usb", "\ue1e0" }, + { "usb_off", "\ue4fa" }, + { "vaccines", "\ue138" }, + { "vape_free", "\uebc6" }, + { "vaping_rooms", "\uebcf" }, + { "verified", "\uef76" }, + { "verified_user", "\ue8e8" }, + { "vertical_align_bottom", "\ue258" }, + { "vertical_align_center", "\ue259" }, + { "vertical_align_top", "\ue25a" }, + { "vertical_distribute", "\ue076" }, + { "vertical_shades", "\uec0e" }, + { "vertical_shades_closed", "\uec0d" }, + { "vertical_split", "\ue949" }, + { "vibration", "\ue62d" }, + { "video_call", "\ue070" }, + { "video_camera_back", "\uf07f" }, + { "video_camera_front", "\uf080" }, + { "video_chat", "\uf8a0" }, + { "video_collection", "\ue04a" }, + { "video_file", "\ueb87" }, + { "video_label", "\ue071" }, + { "video_library", "\ue04a" }, + { "video_settings", "\uea75" }, + { "video_stable", "\uf081" }, + { "videocam", "\ue04b" }, + { "videocam_off", "\ue04c" }, + { "videogame_asset", "\ue338" }, + { "videogame_asset_off", "\ue500" }, + { "view_agenda", "\ue8e9" }, + { "view_array", "\ue8ea" }, + { "view_carousel", "\ue8eb" }, + { "view_column", "\ue8ec" }, + { "view_comfortable", "\ue42a" }, + { "view_comfy", "\ue42a" }, + { "view_comfy_alt", "\ueb73" }, + { "view_compact", "\ue42b" }, + { "view_compact_alt", "\ueb74" }, + { "view_cozy", "\ueb75" }, + { "view_day", "\ue8ed" }, + { "view_headline", "\ue8ee" }, + { "view_in_ar", "\ue9fe" }, + { "view_kanban", "\ueb7f" }, + { "view_list", "\ue8ef" }, + { "view_module", "\ue8f0" }, + { "view_quilt", "\ue8f1" }, + { "view_sidebar", "\uf114" }, + { "view_stream", "\ue8f2" }, + { "view_timeline", "\ueb85" }, + { "view_week", "\ue8f3" }, + { "vignette", "\ue435" }, + { "villa", "\ue586" }, + { "visibility", "\ue8f4" }, + { "visibility_off", "\ue8f5" }, + { "voice_chat", "\ue62e" }, + { "voice_over_off", "\ue94a" }, + { "voicemail", "\ue0d9" }, + { "volcano", "\uebda" }, + { "volume_down", "\ue04d" }, + { "volume_down_alt", "\ue79c" }, + { "volume_mute", "\ue04e" }, + { "volume_off", "\ue04f" }, + { "volume_up", "\ue050" }, + { "volunteer_activism", "\uea70" }, + { "vpn_key", "\ue0da" }, + { "vpn_key_off", "\ueb7a" }, + { "vpn_lock", "\ue62f" }, + { "vrpano", "\uf082" }, + { "wallet", "\uf8ff" }, + { "wallet_giftcard", "\ue8f6" }, + { "wallet_membership", "\ue8f7" }, + { "wallet_travel", "\ue8f8" }, + { "wallpaper", "\ue1bc" }, + { "warehouse", "\uebb8" }, + { "warning", "\ue002" }, + { "warning_amber", "\uf083" }, + { "wash", "\uf1b1" }, + { "watch", "\ue334" }, + { "watch_later", "\ue924" }, + { "watch_off", "\ueae3" }, + { "water", "\uf084" }, + { "water_damage", "\uf203" }, + { "water_drop", "\ue798" }, + { "waterfall_chart", "\uea00" }, + { "waves", "\ue176" }, + { "waving_hand", "\ue766" }, + { "wb_auto", "\ue42c" }, + { "wb_cloudy", "\ue42d" }, + { "wb_incandescent", "\ue42e" }, + { "wb_iridescent", "\ue436" }, + { "wb_shade", "\uea01" }, + { "wb_sunny", "\ue430" }, + { "wb_twighlight", "\uea02" }, + { "wb_twilight", "\ue1c6" }, + { "wc", "\ue63d" }, + { "web", "\ue051" }, + { "web_asset", "\ue069" }, + { "web_asset_off", "\ue4f7" }, + { "web_stories", "\ue595" }, + { "webhook", "\ueb92" }, + { "wechat", "\uea81" }, + { "weekend", "\ue16b" }, + { "west", "\uf1e6" }, + { "whatshot", "\ue80e" }, + { "wheelchair_pickup", "\uf1ab" }, + { "where_to_vote", "\ue177" }, + { "widgets", "\ue1bd" }, + { "width_full", "\uf8f5" }, + { "width_normal", "\uf8f6" }, + { "width_wide", "\uf8f7" }, + { "wifi", "\ue63e" }, + { "wifi_1_bar", "\ue4ca" }, + { "wifi_2_bar", "\ue4d9" }, + { "wifi_calling", "\uef77" }, + { "wifi_calling_1", "\uf0e7" }, + { "wifi_calling_2", "\uf0f6" }, + { "wifi_calling_3", "\uf085" }, + { "wifi_channel", "\ueb6a" }, + { "wifi_find", "\ueb31" }, + { "wifi_lock", "\ue1e1" }, + { "wifi_off", "\ue648" }, + { "wifi_password", "\ueb6b" }, + { "wifi_protected_setup", "\uf0fc" }, + { "wifi_tethering", "\ue1e2" }, + { "wifi_tethering_error", "\uead9" }, + { "wifi_tethering_error_rounded", "\uf086" }, + { "wifi_tethering_off", "\uf087" }, + { "wind_power", "\uec0c" }, + { "window", "\uf088" }, + { "wine_bar", "\uf1e8" }, + { "woman", "\ue13e" }, + { "woman_2", "\uf8e7" }, + { "woo_commerce", "\uea6d" }, + { "wordpress", "\uea9f" }, + { "work", "\ue8f9" }, + { "work_history", "\uec09" }, + { "work_off", "\ue942" }, + { "work_outline", "\ue943" }, + { "workspace_premium", "\ue7af" }, + { "workspaces", "\ue1a0" }, + { "workspaces_filled", "\uea0d" }, + { "workspaces_outline", "\uea0f" }, + { "wrap_text", "\ue25b" }, + { "wrong_location", "\uef78" }, + { "wysiwyg", "\uf1c3" }, + { "yard", "\uf089" }, + { "youtube_searched_for", "\ue8fa" }, + { "zoom_in", "\ue8ff" }, + { "zoom_in_map", "\ueb2d" }, + { "zoom_out", "\ue900" }, + { "zoom_out_map", "\ue56b" } }; + +} + +const QVariantMap& qml_material::IconToken::codeMap() const { return code_map; } \ No newline at end of file diff --git a/src/image.cpp b/src/image.cpp new file mode 100644 index 0000000..a702562 --- /dev/null +++ b/src/image.cpp @@ -0,0 +1,74 @@ +#include "qml_material/image.h" + +#include +#include +#include "core/log.h" + +namespace qml_material +{ + +QImage Round(QImage&& image, CornersMaskRef mask, QRect target) { + if (target.isNull()) { + target = QRect(QPoint(), image.size()); + } else { + _assert_(QRect(QPoint(), image.size()).contains(target)); + } + const auto targetWidth = target.width(); + const auto targetHeight = target.height(); + + image = std::move(image).convertToFormat(QImage::Format_ARGB32_Premultiplied); + _assert_(! image.isNull()); + + // We need to detach image first (if it is shared), before we + // count some offsets using QImage::bytesPerLine etc, because + // bytesPerLine may change on detach, this leads to crashes: + // Real image bytesPerLine is smaller than the one we use for offsets. + auto ints = reinterpret_cast(image.bits()); + + constexpr auto kImageIntsPerPixel = 1; + const auto imageIntsPerLine = (image.bytesPerLine() >> 2); + _assert_(image.depth() == ((kImageIntsPerPixel * sizeof(std::uint32_t)) << 3)); + _assert_(image.bytesPerLine() == (imageIntsPerLine << 2)); + const auto maskCorner = [&](const QImage* mask, bool right = false, bool bottom = false) { + const auto maskWidth = mask ? mask->width() : 0; + const auto maskHeight = mask ? mask->height() : 0; + if (! maskWidth || ! maskHeight || targetWidth < maskWidth || targetHeight < maskHeight) { + return; + } + + const auto maskBytesPerPixel = (mask->depth() >> 3); + const auto maskBytesPerLine = mask->bytesPerLine(); + const auto maskBytesAdded = maskBytesPerLine - maskWidth * maskBytesPerPixel; + _assert_(maskBytesAdded >= 0); + _assert_(mask->depth() == (maskBytesPerPixel << 3)); + const auto imageIntsAdded = imageIntsPerLine - maskWidth * kImageIntsPerPixel; + _assert_(imageIntsAdded >= 0); + auto imageInts = ints + target.x() + target.y() * imageIntsPerLine; + if (right) { + imageInts += targetWidth - maskWidth; + } + if (bottom) { + imageInts += (targetHeight - maskHeight) * imageIntsPerLine; + } + auto maskBytes = mask->constBits(); + for (auto y = 0; y != maskHeight; ++y) { + for (auto x = 0; x != maskWidth; ++x) { + // auto opacity = static_cast(*maskBytes) + 1; + // *imageInts = anim::unshifted(anim::shifted(*imageInts) * opacity); + // maskBytes += maskBytesPerPixel; + // imageInts += kImageIntsPerPixel; + } + maskBytes += maskBytesAdded; + imageInts += imageIntsAdded; + } + }; + + maskCorner(mask.p[0]); + maskCorner(mask.p[1], true); + maskCorner(mask.p[2], false, true); + maskCorner(mask.p[3], true, true); + + return std::move(image); +} + +} // namespace qml_material \ No newline at end of file diff --git a/src/input_block.cpp b/src/input_block.cpp new file mode 100644 index 0000000..52ec06f --- /dev/null +++ b/src/input_block.cpp @@ -0,0 +1,72 @@ +#include "qml_material/input_block.h" + +using namespace qml_material; + +InputBlock::InputBlock(QObject* parent): QObject(parent), mWhen(false), mTarget(nullptr) { + connect(this, &InputBlock::whenChanged, this, &InputBlock::trigger, Qt::QueuedConnection); + connect(this, &InputBlock::targetChanged, this, &InputBlock::trigger, Qt::QueuedConnection); + connect(this, &InputBlock::acceptMouseButtonsChanged, this, &InputBlock::trigger, Qt::QueuedConnection); + connect(this, &InputBlock::acceptHoverEventsChanged, this, &InputBlock::trigger, Qt::QueuedConnection); + connect(this, &InputBlock::acceptTouchEventsChanged, this, &InputBlock::trigger, Qt::QueuedConnection); +} + +bool InputBlock::when() const { return mWhen; } +void InputBlock::setWhen(bool v) { + if (std::exchange(mWhen, v) != v) { + whenChanged(); + } +} + +QQuickItem* InputBlock::target() const { return mTarget; } +void InputBlock::setTarget(QQuickItem* v) { + if (auto old = std::exchange(mTarget, v); old != v) { + if (old) { + mState.restoreState(old); + } + if (mTarget) { + mState.saveState(mTarget); + } + targetChanged(); + } +} + +void InputBlock::State::saveState(QQuickItem* target) { + canHover = target->acceptHoverEvents(); + canTouch = target->acceptTouchEvents(); + mouseButtons = target->acceptedMouseButtons(); +} + +void InputBlock::State::restoreState(QQuickItem* target) { + target->setAcceptedMouseButtons(mouseButtons); + target->setAcceptTouchEvents(canTouch); + target->setAcceptHoverEvents(canHover); +} + +void InputBlock::trigger() { + if (mTarget) { + if (mWhen) { + mReqState.restoreState(mTarget); + } else { + mState.restoreState(mTarget); + } + } +} + +Qt::MouseButtons InputBlock::acceptMouseButtons() const { return mReqState.mouseButtons; } +void InputBlock::setAcceptMouseButtons(Qt::MouseButtons buttons) { + if (std::exchange(mReqState.mouseButtons, buttons)) { + acceptMouseButtonsChanged(); + } +} +bool InputBlock::acceptHoverEvents() const { return mReqState.canHover; } +void InputBlock::setAcceptHoverEvents(bool enabled) { + if (std::exchange(mReqState.canHover, enabled)) { + acceptHoverEventsChanged(); + } +} +bool InputBlock::acceptTouchEvents() const { return mReqState.canTouch; } +void InputBlock::setAcceptTouchEvents(bool accept) { + if (std::exchange(mReqState.canTouch, accept)) { + acceptTouchEventsChanged(); + } +} \ No newline at end of file diff --git a/src/item_holder.cpp b/src/item_holder.cpp new file mode 100644 index 0000000..b23ac44 --- /dev/null +++ b/src/item_holder.cpp @@ -0,0 +1,48 @@ +#include "qml_material/item_holder.h" +#include "core/log.h" +#include + +using namespace qml_material; + +ItemHolder::ItemHolder(QObject* parent): QObject(parent), m_item(nullptr), m_visible(true) { + connect(this, &ItemHolder::itemChanged, this, &ItemHolder::refreshParent, Qt::DirectConnection); +} +ItemHolder::~ItemHolder() {} + +QObject* ItemHolder::item() const { return m_item; } +bool ItemHolder::visible() const { return m_visible; } + +void ItemHolder::setItem(QObject* item) { + if (auto old = std::exchange(m_item, item); old != item) { + _assert_(parent()); + if (old) { + old->setParent(nullptr); + if (auto quick_old = qobject_cast(old)) { + quick_old->setParentItem(nullptr); + } + } + Q_EMIT itemChanged(); + } +} + +void ItemHolder::setVisible(bool v) { + if (std::exchange(m_visible, v) != v) { + if (auto quick_item = qobject_cast(m_item)) { + quick_item->setVisible(visible()); + } + Q_EMIT visibleChanged(); + } +} + +void ItemHolder::refreshParent() { + if (m_item) { + auto quick_item = qobject_cast(m_item); + auto quick_parent = qobject_cast(parent()); + if (quick_item && quick_parent) { + quick_item->setParentItem(quick_parent); + quick_item->setVisible(visible()); + } else { + m_item->setParent(parent()); + } + } +} \ No newline at end of file diff --git a/src/kirigami/wheelhandler.cpp b/src/kirigami/wheelhandler.cpp new file mode 100644 index 0000000..adc7b1a --- /dev/null +++ b/src/kirigami/wheelhandler.cpp @@ -0,0 +1,578 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "kirigami/wheelhandler.h" +// #include "settings.h" +// #include + +#include +#include +#include +#include + +#include + +namespace +{ +bool fuzzyLessThanOrEqualTo(qreal a, qreal b) { + if (a == 0.0 || b == 0.0) { + // qFuzzyCompare is broken + a += 1.0; + b += 1.0; + } + return a <= b || qFuzzyCompare(a, b); +} + +} // namespace + +KirigamiWheelEvent::KirigamiWheelEvent(QObject* parent): QObject(parent) {} + +KirigamiWheelEvent::~KirigamiWheelEvent() {} + +void KirigamiWheelEvent::initializeFromEvent(QWheelEvent* event) { + m_x = event->position().x(); + m_y = event->position().y(); + m_angleDelta = event->angleDelta(); + m_pixelDelta = event->pixelDelta(); + m_buttons = event->buttons(); + m_modifiers = event->modifiers(); + m_accepted = false; + m_inverted = event->inverted(); +} + +qreal KirigamiWheelEvent::x() const { return m_x; } + +qreal KirigamiWheelEvent::y() const { return m_y; } + +QPointF KirigamiWheelEvent::angleDelta() const { return m_angleDelta; } + +QPointF KirigamiWheelEvent::pixelDelta() const { return m_pixelDelta; } + +int KirigamiWheelEvent::buttons() const { return m_buttons; } + +int KirigamiWheelEvent::modifiers() const { return m_modifiers; } + +bool KirigamiWheelEvent::inverted() const { return m_inverted; } + +bool KirigamiWheelEvent::isAccepted() { return m_accepted; } + +void KirigamiWheelEvent::setAccepted(bool accepted) { m_accepted = accepted; } + +/////////////////////////////// + +WheelFilterItem::WheelFilterItem(QQuickItem* parent): QQuickItem(parent) { setEnabled(false); } + +/////////////////////////////// + +WheelHandler::WheelHandler(QObject* parent): QObject(parent) { + m_wheelScrollingTimer.setSingleShot(true); + m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration); + m_wheelScrollingTimer.callOnTimeout([this]() { + setScrolling(false); + }); + + connect(QGuiApplication::styleHints(), + &QStyleHints::wheelScrollLinesChanged, + this, + [this](int scrollLines) { + m_defaultPixelStepSize = 20 * scrollLines; + if (! m_explicitVStepSize && m_verticalStepSize != m_defaultPixelStepSize) { + m_verticalStepSize = m_defaultPixelStepSize; + Q_EMIT verticalStepSizeChanged(); + } + if (! m_explicitHStepSize && m_horizontalStepSize != m_defaultPixelStepSize) { + m_horizontalStepSize = m_defaultPixelStepSize; + Q_EMIT horizontalStepSizeChanged(); + } + }); +} + +WheelHandler::~WheelHandler() {} + +QQuickItem* WheelHandler::target() const { return m_target; } + +void WheelHandler::setTarget(QQuickItem* target) { + if (m_target == target) { + return; + } + + if (target && ! target->inherits("QQuickFlickable")) { + qmlWarning(this) << "target must be a QQuickFlickable"; + return; + } + + if (m_target) { + // disconnect(m_flickable, nullptr, m_filterItem, nullptr); + // disconnect( m_flickable, &QQuickItem::parentChanged, this, + // &WheelHandler::_k_rebindScrollBars); + detach(); + } + + m_target = target; + + const auto qCtx = qmlContext(m_target); + assert(qCtx); + + m_flickable.originX = QQmlProperty(m_target, "originX", qCtx); + m_flickable.originY = QQmlProperty(m_target, "originY", qCtx); + + m_flickable.leftMargin = QQmlProperty(m_target, "leftMargin", qCtx); + m_flickable.rightMargin = QQmlProperty(m_target, "rightMargin", qCtx); + m_flickable.topMargin = QQmlProperty(m_target, "topMargin", qCtx); + m_flickable.bottomMargin = QQmlProperty(m_target, "bottomMargin", qCtx); + + m_flickable.contentX = QQmlProperty(m_target, "contentX", qCtx); + m_flickable.contentY = QQmlProperty(m_target, "contentY", qCtx); + m_flickable.contentHeight = QQmlProperty(m_target, "contentHeight", qCtx); + m_flickable.contentWidth = QQmlProperty(m_target, "contentWidth", qCtx); + m_flickable.height = QQmlProperty(m_target, "height", qCtx); + m_flickable.width = QQmlProperty(m_target, "width", qCtx); + m_flickable.interactive = QQmlProperty(m_target, "interactive", qCtx); + + m_scrollBarV.scrollBar = QQmlProperty(m_target, "ScrollBar.vertical", qCtx); + m_scrollBarH.scrollBar = QQmlProperty(m_target, "ScrollBar.horizontal", qCtx); + + m_flickable.interactive.connectNotifySignal(this, SLOT(refreshAttach())); + m_scrollBarV.scrollBar.connectNotifySignal(this, SLOT(rebindScrollBarV())); + m_scrollBarH.scrollBar.connectNotifySignal(this, SLOT(rebindScrollBarH())); + + refreshAttach(); + /* + m_filterItem->setParentItem(target); + if (target) { + target->installEventFilter(this); + + // Stack WheelFilterItem over the Flickable's scrollable content + m_filterItem->stackAfter(target->property("contentItem").value()); + // Make it fill the Flickable + m_filterItem->setWidth(target->width()); + m_filterItem->setHeight(target->height()); + connect(target, &QQuickItem::widthChanged, m_filterItem, [this, target]() { + m_filterItem->setWidth(target->width()); + }); + connect(target, &QQuickItem::heightChanged, m_filterItem, [this, target]() { + m_filterItem->setHeight(target->height()); + }); + } + */ + rebindScrollBarH(); + rebindScrollBarV(); + Q_EMIT targetChanged(); +} + +void WheelHandler::refreshAttach() { + if (m_flickable.interactive.read().toBool()) { + attach(); + } else { + detach(); + } +} + +void WheelHandler::attach() { + if (m_target) { + m_target->installEventFilter(this); + } +} +void WheelHandler::detach() { + if (m_target) { + m_target->removeEventFilter(this); + } +} + +void WheelHandler::rebindScrollBar(ScrollBar& scrollBar) { + const auto item = scrollBar.scrollBar.read().value(); + + scrollBar.item = item; + + if (item) { + scrollBar.stepSize = QQmlProperty(item, "stepSize", qmlContext(item)); + scrollBar.decreaseMethod = + item->metaObject()->method(item->metaObject()->indexOfMethod("decrease()")); + scrollBar.increaseMethod = + item->metaObject()->method(item->metaObject()->indexOfMethod("increase()")); + } +} + +qreal WheelHandler::verticalStepSize() const { return m_verticalStepSize; } + +void WheelHandler::setVerticalStepSize(qreal stepSize) { + m_explicitVStepSize = true; + if (qFuzzyCompare(m_verticalStepSize, stepSize)) { + return; + } + // Mimic the behavior of QQuickScrollBar when stepSize is 0 + if (qFuzzyIsNull(stepSize)) { + resetVerticalStepSize(); + return; + } + m_verticalStepSize = stepSize; + Q_EMIT verticalStepSizeChanged(); +} + +void WheelHandler::resetVerticalStepSize() { + m_explicitVStepSize = false; + if (qFuzzyCompare(m_verticalStepSize, m_defaultPixelStepSize)) { + return; + } + m_verticalStepSize = m_defaultPixelStepSize; + Q_EMIT verticalStepSizeChanged(); +} + +qreal WheelHandler::horizontalStepSize() const { return m_horizontalStepSize; } + +void WheelHandler::setHorizontalStepSize(qreal stepSize) { + m_explicitHStepSize = true; + if (qFuzzyCompare(m_horizontalStepSize, stepSize)) { + return; + } + // Mimic the behavior of QQuickScrollBar when stepSize is 0 + if (qFuzzyIsNull(stepSize)) { + resetHorizontalStepSize(); + return; + } + m_horizontalStepSize = stepSize; + Q_EMIT horizontalStepSizeChanged(); +} + +void WheelHandler::resetHorizontalStepSize() { + m_explicitHStepSize = false; + if (qFuzzyCompare(m_horizontalStepSize, m_defaultPixelStepSize)) { + return; + } + m_horizontalStepSize = m_defaultPixelStepSize; + Q_EMIT horizontalStepSizeChanged(); +} + +Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const { return m_pageScrollModifiers; } + +void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers) { + if (m_pageScrollModifiers == modifiers) { + return; + } + m_pageScrollModifiers = modifiers; + Q_EMIT pageScrollModifiersChanged(); +} + +void WheelHandler::resetPageScrollModifiers() { + setPageScrollModifiers(m_defaultPageScrollModifiers); +} + +bool WheelHandler::filterMouseEvents() const { return m_filterMouseEvents; } + +void WheelHandler::setFilterMouseEvents(bool enabled) { + if (m_filterMouseEvents == enabled) { + return; + } + m_filterMouseEvents = enabled; + Q_EMIT filterMouseEventsChanged(); +} + +bool WheelHandler::keyNavigationEnabled() const { return m_keyNavigationEnabled; } + +void WheelHandler::setKeyNavigationEnabled(bool enabled) { + if (m_keyNavigationEnabled == enabled) { + return; + } + m_keyNavigationEnabled = enabled; + Q_EMIT keyNavigationEnabledChanged(); +} + +void WheelHandler::classBegin() { + // Initializes smooth scrolling + m_engine = qmlEngine(this); +} + +void WheelHandler::componentComplete() {} + +void WheelHandler::setScrolling(bool scrolling) { + if (m_wheelScrolling == scrolling) { + if (m_wheelScrolling) { + m_wheelScrollingTimer.start(); + } + return; + } + m_wheelScrolling = scrolling; +} + +bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, + Qt::KeyboardModifiers modifiers) { + if (! m_target || (pixelDelta.isNull() && angleDelta.isNull())) { + return false; + } + + bool scrolled { false }; + + auto handler = [this, modifiers, &pixelDelta, &angleDelta, &scrolled](Qt::Orientation ori) { + bool hr { ori == Qt::Horizontal }; + const qreal size = + hr ? m_flickable.width.read().toReal() : m_flickable.height.read().toReal(); + const qreal contentSize = hr ? m_flickable.contentWidth.read().toReal() + : m_flickable.contentHeight.read().toReal(); + const qreal contentPos = + hr ? m_flickable.contentX.read().toReal() : m_flickable.contentY.read().toReal(); + const qreal begginMargin = + hr ? m_flickable.leftMargin.read().toReal() : m_flickable.topMargin.read().toReal(); + const qreal endMargin = + hr ? m_flickable.rightMargin.read().toReal() : m_flickable.bottomMargin.read().toReal(); + const qreal originPos = + hr ? m_flickable.originX.read().toReal() : m_flickable.originY.read().toReal(); + const qreal pageSize = size - begginMargin - endMargin; + const auto window = m_target->window(); + const qreal devicePixelRatio = + window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio(); + + const qreal minExtent = originPos - begginMargin; + const qreal maxExtent = + qMax(minExtent, (contentSize + begginMargin + originPos) - size); + + const bool atBeginning = fuzzyLessThanOrEqualTo(contentPos, minExtent); + const bool atEnd = fuzzyLessThanOrEqualTo(maxExtent, contentPos); + + // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own + // delta transposing + if (modifiers & m_defaultHorizontalScrollModifiers && + qGuiApp->platformName() != QLatin1String("xcb")) { + angleDelta = angleDelta.transposed(); + pixelDelta = pixelDelta.transposed(); + } + + const qreal ticks = (hr ? angleDelta.x() : angleDelta.y()) / 120; + qreal change; + auto stepSize = hr ? m_horizontalStepSize : m_verticalStepSize; + auto& scrollBar = hr ? m_scrollBarH : m_scrollBarV; + + auto& contentPosProp = hr ? m_flickable.contentX : m_flickable.contentY; + + if (contentSize > pageSize) { + // Use page size with pageScrollModifiers. Matches QScrollBar, which uses + // QAbstractSlider behavior. + if (modifiers & m_pageScrollModifiers) { + change = qBound(-pageSize, ticks * pageSize, pageSize); + } else if (pixelDelta.x() != 0) { + change = pixelDelta.x(); + } else { + change = ticks * stepSize; + } + + if (scrollBar.valid()) { + if (((change > 0 && ! atBeginning) || (change < 0 && ! atEnd))) { + const auto _stepSize = scrollBar.stepSize.read().toReal(); + scrollBar.stepSize.write(qBound(0, std::abs(change) / contentSize, 1)); + if (change > 0) + scrollBar.decreaseMethod.invoke(scrollBar.item); + else + scrollBar.increaseMethod.invoke(scrollBar.item); + scrollBar.stepSize.write(_stepSize); + scrolled = true; + } + } else { + // contentX and contentY use reversed signs from what x and y would normally use, so + // flip the signs + + qreal newContentPos = std::clamp(contentPos - change, minExtent, maxExtent); + + // Flickable::pixelAligned rounds the position, so round to mimic that behavior. + // Rounding prevents fractional positioning from causing text to be + // clipped off on the top and bottom. + // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio + // after to make position match pixels on the screen more closely. + newContentPos = std::round(newContentPos * devicePixelRatio) / devicePixelRatio; + if (contentPos != newContentPos) { + scrolled = true; + contentPosProp.write(newContentPos); + } + } + } + }; + + handler(Qt::Horizontal); + handler(Qt::Vertical); + + return scrolled; +} + +bool WheelHandler::scrollUp(qreal stepSize) { + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_verticalStepSize; + } + // contentY uses reversed sign + return scrollFlickable(QPointF(0, stepSize)); +} + +bool WheelHandler::scrollDown(qreal stepSize) { + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_verticalStepSize; + } + // contentY uses reversed sign + return scrollFlickable(QPointF(0, -stepSize)); +} + +bool WheelHandler::scrollLeft(qreal stepSize) { + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_horizontalStepSize; + } + // contentX uses reversed sign + return scrollFlickable(QPoint(stepSize, 0)); +} + +bool WheelHandler::scrollRight(qreal stepSize) { + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_horizontalStepSize; + } + // contentX uses reversed sign + return scrollFlickable(QPoint(-stepSize, 0)); +} + +bool WheelHandler::eventFilter(QObject* watched, QEvent* event) { + auto item = qobject_cast(watched); + if (! item || ! item->isEnabled()) { + return false; + } + + qreal contentWidth = 0; + qreal contentHeight = 0; + qreal pageWidth = 0; + qreal pageHeight = 0; + if (m_target) { + contentWidth = m_flickable.contentWidth.read().toReal(); + contentHeight = m_flickable.contentHeight.read().toReal(); + pageWidth = m_flickable.width.read().toReal() - m_flickable.leftMargin.read().toReal() - + m_flickable.rightMargin.read().toReal(); + pageHeight = m_flickable.height.read().toReal() - m_flickable.topMargin.read().toReal() - + m_flickable.bottomMargin.read().toReal(); + } + + // The code handling touch, mouse and hover events is mostly copied/adapted from + // QQuickScrollView::childMouseEventFilter() + switch (event->type()) { + case QEvent::Wheel: { + QWheelEvent* wheelEvent = static_cast(event); + + // Can't use wheelEvent->deviceType() to determine device type since on Wayland mouse is + // always regarded as touchpad + // https://invent.kde.org/qt/qt/qtwayland/-/blob/e695a39519a7629c1549275a148cfb9ab99a07a9/src/client/qwaylandinputdevice.cpp#L445 + // and we can only expect a touchpad never generates the same angle delta as a mouse + // m_wasTouched = std::abs(wheelEvent->angleDelta().y()) != 120 && + // std::abs(wheelEvent->angleDelta().x()) != 120; + m_wasTouched = false; + + // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse + // that shouldn't use pixelDelta. If faulty pixelDelta, reset pixelDelta to (0,0). + if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) { + // In order to change any of the data, we have to create a whole new QWheelEvent + // from its constructor. + QWheelEvent newWheelEvent(wheelEvent->position(), + wheelEvent->globalPosition(), + QPoint(0, 0), // pixelDelta + wheelEvent->angleDelta(), + wheelEvent->buttons(), + wheelEvent->modifiers(), + wheelEvent->phase(), + wheelEvent->inverted(), + wheelEvent->source()); + m_kirigamiWheelEvent.initializeFromEvent(&newWheelEvent); + } else { + m_kirigamiWheelEvent.initializeFromEvent(wheelEvent); + } + + Q_EMIT wheel(&m_kirigamiWheelEvent); + + if (m_kirigamiWheelEvent.isAccepted()) { + return true; + } + + bool scrolled = false; + if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) { + // Don't use pixelDelta from the event unless angleDelta is not available + // because scrolling by pixelDelta is too slow on Wayland with libinput. + QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() + ? m_kirigamiWheelEvent.pixelDelta() + : QPoint(0, 0); + scrolled = scrollFlickable(pixelDelta, + m_kirigamiWheelEvent.angleDelta(), + Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers())); + } + setScrolling(scrolled); + + // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause + // scrolling to jump back to where scrolling started unless the event is always accepted + // before it reaches the Flickable. + bool flickableWillUseGestureScrolling = + ! (wheelEvent->source() == Qt::MouseEventNotSynthesized || + wheelEvent->pixelDelta().isNull()); + + if(scrolled) { + Q_EMIT wheelMoved(); + } + + return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling; + } + + case QEvent::TouchBegin: { + m_wasTouched = true; + break; + } + + case QEvent::TouchEnd: { + m_wasTouched = false; + break; + } + + case QEvent::MouseMove: + case QEvent::MouseButtonRelease: { + setScrolling(false); + break; + } + + case QEvent::KeyPress: { + if (! m_keyNavigationEnabled) { + break; + } + QKeyEvent* keyEvent = static_cast(event); + bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers; + switch (keyEvent->key()) { + case Qt::Key_Up: return scrollUp(); + case Qt::Key_Down: return scrollDown(); + case Qt::Key_Left: return scrollLeft(); + case Qt::Key_Right: return scrollRight(); + case Qt::Key_PageUp: return horizontalScroll ? scrollLeft(pageWidth) : scrollUp(pageHeight); + case Qt::Key_PageDown: + return horizontalScroll ? scrollRight(pageWidth) : scrollDown(pageHeight); + case Qt::Key_Home: + return horizontalScroll ? scrollLeft(contentWidth) : scrollUp(contentHeight); + case Qt::Key_End: + return horizontalScroll ? scrollRight(contentWidth) : scrollDown(contentHeight); + default: break; + } + break; + } + + default: break; + } + + return false; +} + +bool WheelHandler::useAnimation() const { return m_useAnimation; } + +void WheelHandler::setUseAnimation(bool v) { + if (std::exchange(m_useAnimation, v) != v) { + Q_EMIT useAnimationChanged(); + } +} + +void WheelHandler::rebindScrollBarV() { rebindScrollBar(m_scrollBarV); } +void WheelHandler::rebindScrollBarH() { rebindScrollBar(m_scrollBarH); } + +// #include "moc_wheelhandler.cpp" diff --git a/src/round_item.cpp b/src/round_item.cpp new file mode 100644 index 0000000..6289934 --- /dev/null +++ b/src/round_item.cpp @@ -0,0 +1,46 @@ +#include "qml_material/round_item.h" + +#include + +using namespace qml_material; + +namespace +{ +void updateGeometry(QSGGeometry *g, double radius_, QRectF rect) { + int vertexCount = 0; + + // Radius should never exceeds half of the width or half of the height + qreal radius = qMin(qMin(rect.width() / 2, rect.height() / 2), radius_); + rect.adjust(radius, radius, -radius, -radius); + + int segments = qMin(30, qCeil(radius)); // Number of segments per corner. + + g->allocate((segments + 1) * 4); + + QVector2D* vertices = (QVector2D*)g->vertexData(); + + for (int part = 0; part < 2; ++part) { + for (int i = 0; i <= segments; ++i) { + // ### Should change to calculate sin/cos only once. + qreal angle = qreal(0.5 * M_PI) * (part + i / qreal(segments)); + qreal s = qFastSin(angle); + qreal c = qFastCos(angle); + qreal y = + (part ? rect.bottom() : rect.top()) - radius * c; // current inner y-coordinate. + qreal lx = rect.left() - radius * s; // current inner left x-coordinate. + qreal rx = rect.right() + radius * s; // current inner right x-coordinate. + + vertices[vertexCount++] = QVector2D(rx, y); + vertices[vertexCount++] = QVector2D(lx, y); + } + } +} +} // namespace + +RoundItem::RoundItem(QQuickItem* parent): QQuickItem(parent) {} + +RoundItem::~RoundItem() {} + +QSGNode* RoundItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + +} \ No newline at end of file diff --git a/src/state.cpp b/src/state.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/theme.cpp b/src/theme.cpp new file mode 100644 index 0000000..deed129 --- /dev/null +++ b/src/theme.cpp @@ -0,0 +1,109 @@ +#include "qml_material/theme.h" + +using namespace qml_material; + +namespace +{ +using Propagator = QQuickAttachedPropertyPropagator; + +template +concept get_prop_cp = requires(F f, Theme* t) { + { std::invoke(f, t) } -> std::same_as&>; +}; + +template +void propagate(Theme* self, F&& f) { + const auto styles = self->attachedChildren(); + for (auto* child : styles) { + Theme* obj = qobject_cast(child); + if (obj) { + f(obj); + } + } +} + +template + requires get_prop_cp +void inherit_attach_prop(Theme* self, F&& get_prop, const T& v) { + auto& p = std::invoke(get_prop, self); + if (p.explicited || p.value == v) return; + p.value = v; + propagate(self, [&v, &get_prop](Theme* child) { + inherit_attach_prop(child, get_prop, v); + }); + std::invoke(p.sig_func, self); +} + +template + requires get_prop_cp +void set_prop(Theme* self, const T& v, F&& get_prop) { + auto& p = std::invoke(std::forward(get_prop), self); + p.explicited = true; + if (std::exchange(p.value, v) != v) { + propagate(self, [&v, &get_prop](Theme* child) { + inherit_attach_prop(child, get_prop, v); + }); + std::invoke(p.sig_func, self); + } +} + +template + requires get_prop_cp +void reset_prop(Theme* self, F&& get_prop, const T& init_v) { + auto& p = std::invoke(std::forward(get_prop), self); + if (! p.explicited) return; + p.explicited = false; + inherit_attach_prop(self, get_prop, init_v); +} + +struct GlobalTheme { + QColor textColor; + QColor supportTextColor; + QColor backgroundColor; + QColor stateLayerColor; + int elevation { 0 }; + MdColorMgr color_; + MdColorMgr* color { &color_ }; +}; +Q_GLOBAL_STATIC(GlobalTheme, theGlobalTheme) + +} // namespace + +Theme::Theme(QObject* parent): QQuickAttachedPropertyPropagator(parent) { + QQuickAttachedPropertyPropagator::initialize(); +} +Theme::~Theme() {} + +Theme* Theme::qmlAttachedProperties(QObject* object) { return new Theme(object); } + +#define IMPL_ATTACH_PROP(_type_, _name_, _prop_, ...) \ + Theme::AttachProp<_type_>& Theme::get_##_name_() { return _prop_; } \ + _type_ Theme::_name_() const { return _prop_.value.value_or(theGlobalTheme->_name_); } \ + void Theme::set_##_name_(_type_ v) { set_prop(this, v, &Theme::get_##_name_); } \ + void Theme::reset_##_name_() { \ + Self* obj = qobject_cast(attachedParent()); \ + reset_prop(this, &Theme::get_##_name_, obj ? obj->_name_() : theGlobalTheme->_name_); \ + } + +IMPL_ATTACH_PROP(QColor, textColor, m_textColor) +IMPL_ATTACH_PROP(QColor, supportTextColor, m_supportTextColor) +IMPL_ATTACH_PROP(QColor, backgroundColor, m_backgroundColor) +IMPL_ATTACH_PROP(int, elevation, m_elevation) +IMPL_ATTACH_PROP(QColor, stateLayerColor, m_stateLayerColor) +IMPL_ATTACH_PROP(MdColorMgr*, color, m_color) + +void Theme::attachedParentChange(QQuickAttachedPropertyPropagator* newParent, + QQuickAttachedPropertyPropagator* oldParent) { + Propagator::attachedParentChange(newParent, oldParent); + Theme* attachedParentStyle = qobject_cast(newParent); + if (attachedParentStyle) { +#define X(_name_) inherit_attach_prop(this, &Theme::get_##_name_, attachedParentStyle->_name_()) + X(textColor); + X(supportTextColor); + X(backgroundColor); + X(stateLayerColor); + X(elevation); + X(color); +#undef X + } +} \ No newline at end of file diff --git a/src/type.cpp b/src/type.cpp new file mode 100644 index 0000000..1a7f82c --- /dev/null +++ b/src/type.cpp @@ -0,0 +1 @@ +#include "qml_material/type.h" \ No newline at end of file diff --git a/src/type_scale.cpp b/src/type_scale.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..bf9d50c --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,183 @@ +#include "qml_material/util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "core/log.h" +#include "core/qstr_helper.h" +#include "core/lambda_hlper.h" + +Q_GLOBAL_STATIC(qml_material::Xdp, TheXdp) + +namespace +{ +inline constexpr auto kService = "org.freedesktop.portal.Desktop"; +inline constexpr auto kObjectPath = "/org/freedesktop/portal/desktop"; +inline constexpr auto kRequestInterface = "org.freedesktop.portal.Request"; +inline constexpr auto kSettingsInterface = "org.freedesktop.portal.Settings"; + +Qt::ColorScheme to_color_scheme(const QVariant& in) { + auto v = in.toUInt(); + return v == 1 ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; +} + +QColor to_accent_color(const QVariant& in) { + std::array c { 0 }; + const auto v = in.value(); + v.beginStructure(); + v >> c[0] >> c[1] >> c[2]; + v.endStructure(); + return QColor::fromRgbF(c[0], c[1], c[2]); +} +} // namespace + +namespace qml_material +{ +Xdp::Xdp(QObject* parent): QObject(parent) { + auto bus = QDBusConnection::sessionBus(); + auto res = bus.connect(kService, + kObjectPath, + kSettingsInterface, + "SettingChanged", + this, + SLOT(xdpSettingChangeSlot(QString, QString, QDBusVariant))); + _assert_(res); + + auto message = + QDBusMessage::createMethodCall(kService, kObjectPath, kSettingsInterface, "Read"); + + { + message << "org.freedesktop.appearance" + << "color-scheme"; + // this must not be asyncCall() because we have to set appearance now + QDBusReply reply = QDBusConnection::sessionBus().call(message); + if (reply.isValid()) { + const QDBusVariant dbusVariant = qvariant_cast(reply.value()); + m_color_scheme = to_color_scheme(dbusVariant.variant()); + } + } + + { + message.setArguments({ "org.freedesktop.appearance", "accent-color" }); + QDBusReply reply = QDBusConnection::sessionBus().call(message); + if (reply.isValid()) { + const QDBusVariant dbusVariant = qvariant_cast(reply.value()); + m_accent_color = to_accent_color(dbusVariant.variant()); + } + } +} +Xdp::~Xdp() {} + +Xdp* Xdp::insance() { return TheXdp; } + +void Xdp::xdpSettingChangeSlot(QString namespace_, QString key, QDBusVariant value_) { + auto value = value_.variant(); + if (namespace_ == "org.freedesktop.appearance" && key == "color-scheme") { + m_color_scheme = to_color_scheme(value); + Q_EMIT colorSchemeChanged(); + } else if (namespace_ == "org.freedesktop.appearance" && key == "accent-color") { + m_accent_color = to_accent_color(value); + Q_EMIT accentColorChanged(); + } + + DEBUG_LOG("xdp SettingChanged: {} {}, v({}): {}", + namespace_, + key, + value.metaType().name(), + value.toString()); +} + +QColor Xdp::accentColor() const { return m_accent_color.value_or(QColor {}); } + +Qt::ColorScheme Xdp::colorScheme() const { + return m_color_scheme ? m_color_scheme.value() : QGuiApplication::styleHints()->colorScheme(); +} + +Util::Util(QObject* parent): QObject(parent) {} +Util::~Util() {} + +CornersGroup Util::corner(QVariant in) { + CornersGroup out; + if (in.canConvert()) { + out = CornersGroup(in.value()); + } else if (auto list = in.toList(); ! list.empty()) { + switch (list.size()) { + case 1: { + out = CornersGroup(list[0].value()); + break; + } + case 2: { + out.setTopLeft(list[0].value()); + out.setTopRight(list[0].value()); + out.setBottomLeft(list[1].value()); + out.setBottomRight(list[1].value()); + break; + } + case 3: { + out.setTopLeft(list[0].value()); + out.setTopRight(list[1].value()); + out.setBottomLeft(list[2].value()); + out.setBottomRight(list[1].value()); + break; + } + default: + case 4: { + out.setTopLeft(list[0].value()); + out.setTopRight(list[1].value()); + out.setBottomLeft(list[2].value()); + out.setBottomRight(list[3].value()); + } + } + } + return out; +} + +CornersGroup Util::corner(qreal br, qreal tr, qreal bl, qreal tl) { + return CornersGroup(br, tr, bl, tl); +} + +void Util::track(QVariant, Track t) { + switch (t) { + case TrackCreate: + m_tracked++; + WARN_LOG("track create {}", m_tracked); + break; + case TrackDelete: + m_tracked--; + WARN_LOG("track delete {}", m_tracked); + break; + } +} + + +QString Util::type_str(const QJSValue& obj) { + if(obj.isQObject()) { + return obj.toQObject()->metaObject()->className(); + } + if(obj.isVariant()) { + return obj.toVariant().metaType().name(); + } + if(auto objname = obj.property("objectName").toString(); !objname.isEmpty()) { + return objname; + } + return obj.toString(); +} + +void Util::print_parents(const QJSValue& obj) { + auto cur = obj; + auto format_parent = core::y_combinator {[this](auto format_parent, const QJSValue& cur, i32 level) -> std::string { + if(!cur.isNull()) { + return fmt::format(" {}\n{}", type_str(cur), format_parent(cur.property("parent"), level + 1)); + } + return {}; + }}; + DEBUG_LOG("{}\n{}", type_str(obj), format_parent(obj.property("parent"), 1)); +} + +} // namespace qml_material \ No newline at end of file diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..9137283 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,13 @@ +set(md_dir material-color/cpp) +add_library( + material_color STATIC + ${md_dir}/scheme/scheme.cc + ${md_dir}/palettes/core.cc + ${md_dir}/palettes/tones.cc + ${md_dir}/utils/utils.cc + ${md_dir}/cam/cam.cc + ${md_dir}/cam/hct.cc + ${md_dir}/cam/hct_solver.cc + ${md_dir}/cam/viewing_conditions.cc + ${md_dir}/blend/blend.cc) +target_include_directories(material_color PUBLIC material-color) \ No newline at end of file diff --git a/third_party/material-color/LICENSE b/third_party/material-color/LICENSE new file mode 100644 index 0000000..3879bcb --- /dev/null +++ b/third_party/material-color/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/material-color/cpp/blend/blend.cc b/third_party/material-color/cpp/blend/blend.cc new file mode 100644 index 0000000..4b8c34b --- /dev/null +++ b/third_party/material-color/cpp/blend/blend.cc @@ -0,0 +1,71 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/blend/blend.h" + +#include +#include + +#include "cpp/cam/cam.h" +#include "cpp/cam/hct.h" +#include "cpp/cam/viewing_conditions.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +Argb BlendHarmonize(const Argb design_color, const Argb key_color) { + Hct from_hct(design_color); + Hct to_hct(key_color); + double difference_degrees = DiffDegrees(from_hct.get_hue(), to_hct.get_hue()); + double rotation_degrees = std::min(difference_degrees * 0.5, 15.0); + double output_hue = SanitizeDegreesDouble( + from_hct.get_hue() + + rotation_degrees * + RotationDirection(from_hct.get_hue(), to_hct.get_hue())); + from_hct.set_hue(output_hue); + return from_hct.ToInt(); +} + +Argb BlendHctHue(const Argb from, const Argb to, const double amount) { + int ucs = BlendCam16Ucs(from, to, amount); + Hct ucs_hct(ucs); + Hct from_hct(from); + from_hct.set_hue(ucs_hct.get_hue()); + return from_hct.ToInt(); +} + +Argb BlendCam16Ucs(const Argb from, const Argb to, const double amount) { + Cam from_cam = CamFromInt(from); + Cam to_cam = CamFromInt(to); + + const double a_j = from_cam.jstar; + const double a_a = from_cam.astar; + const double a_b = from_cam.bstar; + + const double b_j = to_cam.jstar; + const double b_a = to_cam.astar; + const double b_b = to_cam.bstar; + + const double jstar = a_j + (b_j - a_j) * amount; + const double astar = a_a + (b_a - a_a) * amount; + const double bstar = a_b + (b_b - a_b) * amount; + + const Cam blended = CamFromUcsAndViewingConditions(jstar, astar, bstar, + kDefaultViewingConditions); + return IntFromCam(blended); +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/blend/blend.h b/third_party/material-color/cpp/blend/blend.h new file mode 100644 index 0000000..ca730ae --- /dev/null +++ b/third_party/material-color/cpp/blend/blend.h @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_BLEND_BLEND_H_ +#define CPP_BLEND_BLEND_H_ + +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +Argb BlendHarmonize(const Argb design_color, const Argb key_color); +Argb BlendHctHue(const Argb from, const Argb to, const double amount); +Argb BlendCam16Ucs(const Argb from, const Argb to, const double amount); + +} // namespace material_color_utilities +#endif // CPP_BLEND_BLEND_H_ diff --git a/third_party/material-color/cpp/blend/blend_test.cc b/third_party/material-color/cpp/blend/blend_test.cc new file mode 100644 index 0000000..20462d2 --- /dev/null +++ b/third_party/material-color/cpp/blend/blend_test.cc @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/blend/blend.h" + +#include "testing/base/public/gunit.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { +TEST(BlendTest, RedToBlue) { + int blended = BlendHctHue(0xffff0000, 0xff0000ff, 0.8); + EXPECT_EQ(HexFromArgb(blended), "ff905eff"); +} +} // namespace + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/cam.cc b/third_party/material-color/cpp/cam/cam.cc new file mode 100644 index 0000000..769cd1a --- /dev/null +++ b/third_party/material-color/cpp/cam/cam.cc @@ -0,0 +1,263 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/cam.h" + +#include +#include + +#include "cpp/cam/hct_solver.h" +#include "cpp/cam/viewing_conditions.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +Cam CamFromJchAndViewingConditions(double j, double c, double h, + ViewingConditions viewing_conditions); + +Cam CamFromUcsAndViewingConditions( + double jstar, double astar, double bstar, + const ViewingConditions &viewing_conditions) { + const double a = astar; + const double b = bstar; + const double m = sqrt(a * a + b * b); + const double m_2 = (exp(m * 0.0228) - 1.0) / 0.0228; + const double c = m_2 / viewing_conditions.fl_root; + double h = atan2(b, a) * (180.0 / kPi); + if (h < 0.0) { + h += 360.0; + } + const double j = jstar / (1 - (jstar - 100) * 0.007); + return CamFromJchAndViewingConditions(j, c, h, viewing_conditions); +} + +Cam CamFromIntAndViewingConditions( + Argb argb, const ViewingConditions &viewing_conditions) { + // XYZ from ARGB, inlined. + int red = (argb & 0x00ff0000) >> 16; + int green = (argb & 0x0000ff00) >> 8; + int blue = (argb & 0x000000ff); + double red_l = Linearized(red); + double green_l = Linearized(green); + double blue_l = Linearized(blue); + double x = 0.41233895 * red_l + 0.35762064 * green_l + 0.18051042 * blue_l; + double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l; + double z = 0.01932141 * red_l + 0.11916382 * green_l + 0.95034478 * blue_l; + + // Convert XYZ to 'cone'/'rgb' responses + double r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z; + double g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z; + double b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z; + + // Discount illuminant. + double r_d = viewing_conditions.rgb_d[0] * r_c; + double g_d = viewing_conditions.rgb_d[1] * g_c; + double b_d = viewing_conditions.rgb_d[2] * b_c; + + // Chromatic adaptation. + double r_af = pow(viewing_conditions.fl * fabs(r_d) / 100.0, 0.42); + double g_af = pow(viewing_conditions.fl * fabs(g_d) / 100.0, 0.42); + double b_af = pow(viewing_conditions.fl * fabs(b_d) / 100.0, 0.42); + double r_a = Signum(r_d) * 400.0 * r_af / (r_af + 27.13); + double g_a = Signum(g_d) * 400.0 * g_af / (g_af + 27.13); + double b_a = Signum(b_d) * 400.0 * b_af / (b_af + 27.13); + + // Redness-greenness + double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0; + double b = (r_a + g_a - 2.0 * b_a) / 9.0; + double u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0; + double p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0; + + double radians = atan2(b, a); + double degrees = radians * 180.0 / kPi; + double hue = SanitizeDegreesDouble(degrees); + double hue_radians = hue * kPi / 180.0; + double ac = p2 * viewing_conditions.nbb; + + double j = 100.0 * pow(ac / viewing_conditions.aw, + viewing_conditions.c * viewing_conditions.z); + double q = (4.0 / viewing_conditions.c) * sqrt(j / 100.0) * + (viewing_conditions.aw + 4.0) * viewing_conditions.fl_root; + double hue_prime = hue < 20.14 ? hue + 360 : hue; + double e_hue = 0.25 * (cos(hue_prime * kPi / 180.0 + 2.0) + 3.8); + double p1 = + 50000.0 / 13.0 * e_hue * viewing_conditions.n_c * viewing_conditions.ncb; + double t = p1 * sqrt(a * a + b * b) / (u + 0.305); + double alpha = + pow(t, 0.9) * + pow(1.64 - pow(0.29, viewing_conditions.background_y_to_white_point_y), + 0.73); + double c = alpha * sqrt(j / 100.0); + double m = c * viewing_conditions.fl_root; + double s = 50.0 * sqrt((alpha * viewing_conditions.c) / + (viewing_conditions.aw + 4.0)); + double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); + double mstar = 1.0 / 0.0228 * log(1.0 + 0.0228 * m); + double astar = mstar * cos(hue_radians); + double bstar = mstar * sin(hue_radians); + return {hue, c, j, q, m, s, jstar, astar, bstar}; +} + +Cam CamFromInt(Argb argb) { + return CamFromIntAndViewingConditions(argb, kDefaultViewingConditions); +} + +Argb IntFromCamAndViewingConditions(Cam cam, + ViewingConditions viewing_conditions) { + double alpha = (cam.chroma == 0.0 || cam.j == 0.0) + ? 0.0 + : cam.chroma / sqrt(cam.j / 100.0); + double t = pow( + alpha / pow(1.64 - pow(0.29, + viewing_conditions.background_y_to_white_point_y), + 0.73), + 1.0 / 0.9); + double h_rad = cam.hue * kPi / 180.0; + double e_hue = 0.25 * (cos(h_rad + 2.0) + 3.8); + double ac = + viewing_conditions.aw * + pow(cam.j / 100.0, 1.0 / viewing_conditions.c / viewing_conditions.z); + double p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.n_c * + viewing_conditions.ncb; + double p2 = ac / viewing_conditions.nbb; + double h_sin = sin(h_rad); + double h_cos = cos(h_rad); + double gamma = 23.0 * (p2 + 0.305) * t / + (23.0 * p1 + 11.0 * t * h_cos + 108.0 * t * h_sin); + double a = gamma * h_cos; + double b = gamma * h_sin; + double r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0; + double g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0; + double b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0; + + double r_c_base = fmax(0, (27.13 * fabs(r_a)) / (400.0 - fabs(r_a))); + double r_c = + Signum(r_a) * (100.0 / viewing_conditions.fl) * pow(r_c_base, 1.0 / 0.42); + double g_c_base = fmax(0, (27.13 * fabs(g_a)) / (400.0 - fabs(g_a))); + double g_c = + Signum(g_a) * (100.0 / viewing_conditions.fl) * pow(g_c_base, 1.0 / 0.42); + double b_c_base = fmax(0, (27.13 * fabs(b_a)) / (400.0 - fabs(b_a))); + double b_c = + Signum(b_a) * (100.0 / viewing_conditions.fl) * pow(b_c_base, 1.0 / 0.42); + double r_x = r_c / viewing_conditions.rgb_d[0]; + double g_x = g_c / viewing_conditions.rgb_d[1]; + double b_x = b_c / viewing_conditions.rgb_d[2]; + double x = 1.86206786 * r_x - 1.01125463 * g_x + 0.14918677 * b_x; + double y = 0.38752654 * r_x + 0.62144744 * g_x - 0.00897398 * b_x; + double z = -0.01584150 * r_x - 0.03412294 * g_x + 1.04996444 * b_x; + + // intFromXyz + double r_l = 3.2406 * x - 1.5372 * y - 0.4986 * z; + double g_l = -0.9689 * x + 1.8758 * y + 0.0415 * z; + double b_l = 0.0557 * x - 0.2040 * y + 1.0570 * z; + + int red = Delinearized(r_l); + int green = Delinearized(g_l); + int blue = Delinearized(b_l); + + return ArgbFromRgb(red, green, blue); +} + +Argb IntFromCam(Cam cam) { + return IntFromCamAndViewingConditions(cam, kDefaultViewingConditions); +} + +Cam CamFromJchAndViewingConditions(double j, double c, double h, + ViewingConditions viewing_conditions) { + double q = (4.0 / viewing_conditions.c) * sqrt(j / 100.0) * + (viewing_conditions.aw + 4.0) * (viewing_conditions.fl_root); + double m = c * viewing_conditions.fl_root; + double alpha = c / sqrt(j / 100.0); + double s = 50.0 * sqrt((alpha * viewing_conditions.c) / + (viewing_conditions.aw + 4.0)); + double hue_radians = h * kPi / 180.0; + double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); + double mstar = 1.0 / 0.0228 * log(1.0 + 0.0228 * m); + double astar = mstar * cos(hue_radians); + double bstar = mstar * sin(hue_radians); + return {h, c, j, q, m, s, jstar, astar, bstar}; +} + +double CamDistance(Cam a, Cam b) { + double d_j = a.jstar - b.jstar; + double d_a = a.astar - b.astar; + double d_b = a.bstar - b.bstar; + double d_e_prime = sqrt(d_j * d_j + d_a * d_a + d_b * d_b); + double d_e = 1.41 * pow(d_e_prime, 0.63); + return d_e; +} + +Argb IntFromHcl(double hue, double chroma, double lstar) { + return SolveToInt(hue, chroma, lstar); +} + +Cam CamFromXyzAndViewingConditions( + double x, double y, double z, const ViewingConditions &viewing_conditions) { + // Convert XYZ to 'cone'/'rgb' responses + double r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z; + double g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z; + double b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z; + + // Discount illuminant. + double r_d = viewing_conditions.rgb_d[0] * r_c; + double g_d = viewing_conditions.rgb_d[1] * g_c; + double b_d = viewing_conditions.rgb_d[2] * b_c; + + // Chromatic adaptation. + double r_af = pow(viewing_conditions.fl * fabs(r_d) / 100.0, 0.42); + double g_af = pow(viewing_conditions.fl * fabs(g_d) / 100.0, 0.42); + double b_af = pow(viewing_conditions.fl * fabs(b_d) / 100.0, 0.42); + double r_a = Signum(r_d) * 400.0 * r_af / (r_af + 27.13); + double g_a = Signum(g_d) * 400.0 * g_af / (g_af + 27.13); + double b_a = Signum(b_d) * 400.0 * b_af / (b_af + 27.13); + + // Redness-greenness + double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0; + double b = (r_a + g_a - 2.0 * b_a) / 9.0; + double u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0; + double p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0; + + double radians = atan2(b, a); + double degrees = radians * 180.0 / kPi; + double hue = SanitizeDegreesDouble(degrees); + double hue_radians = hue * kPi / 180.0; + double ac = p2 * viewing_conditions.nbb; + + double j = 100.0 * pow(ac / viewing_conditions.aw, + viewing_conditions.c * viewing_conditions.z); + double q = (4.0 / viewing_conditions.c) * sqrt(j / 100.0) * + (viewing_conditions.aw + 4.0) * viewing_conditions.fl_root; + double hue_prime = hue < 20.14 ? hue + 360 : hue; + double e_hue = 0.25 * (cos(hue_prime * kPi / 180.0 + 2.0) + 3.8); + double p1 = + 50000.0 / 13.0 * e_hue * viewing_conditions.n_c * viewing_conditions.ncb; + double t = p1 * sqrt(a * a + b * b) / (u + 0.305); + double alpha = + pow(t, 0.9) * + pow(1.64 - pow(0.29, viewing_conditions.background_y_to_white_point_y), + 0.73); + double c = alpha * sqrt(j / 100.0); + double m = c * viewing_conditions.fl_root; + double s = 50.0 * sqrt((alpha * viewing_conditions.c) / + (viewing_conditions.aw + 4.0)); + double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); + double mstar = 1.0 / 0.0228 * log(1.0 + 0.0228 * m); + double astar = mstar * cos(hue_radians); + double bstar = mstar * sin(hue_radians); + return {hue, c, j, q, m, s, jstar, astar, bstar}; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/cam.h b/third_party/material-color/cpp/cam/cam.h new file mode 100644 index 0000000..2ff5db5 --- /dev/null +++ b/third_party/material-color/cpp/cam/cam.h @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_CAM_CAM_H_ +#define CPP_CAM_CAM_H_ + +#include "cpp/cam/viewing_conditions.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +struct Cam { + double hue = 0.0; + double chroma = 0.0; + double j = 0.0; + double q = 0.0; + double m = 0.0; + double s = 0.0; + + double jstar = 0.0; + double astar = 0.0; + double bstar = 0.0; +}; + +Cam CamFromInt(Argb argb); +Cam CamFromIntAndViewingConditions(Argb argb, + const ViewingConditions &viewing_conditions); +Argb IntFromHcl(double hue, double chroma, double lstar); +Argb IntFromCam(Cam cam); +Cam CamFromUcsAndViewingConditions(double jstar, double astar, double bstar, + const ViewingConditions &viewing_conditions); +/** + * Given color expressed in the XYZ color space and viewed + * in [viewingConditions], converts the color to CAM16. + */ +Cam CamFromXyzAndViewingConditions(double x, double y, double z, + const ViewingConditions &viewing_conditions); + +} // namespace material_color_utilities +#endif // CPP_CAM_CAM_H_ diff --git a/third_party/material-color/cpp/cam/cam_test.cc b/third_party/material-color/cpp/cam/cam_test.cc new file mode 100644 index 0000000..6dde7de --- /dev/null +++ b/third_party/material-color/cpp/cam/cam_test.cc @@ -0,0 +1,109 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/cam.h" + +#include "testing/base/public/gmock.h" +#include "testing/base/public/gunit.h" + +namespace material_color_utilities { + +namespace { +using testing::DoubleNear; + +using testing::Eq; + +Argb RED = 0xffff0000; +Argb GREEN = 0xff00ff00; +Argb BLUE = 0xff0000ff; +Argb WHITE = 0xffffffff; +Argb BLACK = 0xff000000; + +TEST(CamTest, Red) { + Cam cam = CamFromInt(RED); + + EXPECT_THAT(cam.hue, DoubleNear(27.408, 0.001)); + EXPECT_THAT(cam.chroma, DoubleNear(113.357, 0.001)); + EXPECT_THAT(cam.j, DoubleNear(46.445, 0.001)); + EXPECT_THAT(cam.m, DoubleNear(89.494, 0.001)); + EXPECT_THAT(cam.s, DoubleNear(91.889, 0.001)); + EXPECT_THAT(cam.q, DoubleNear(105.988, 0.001)); +} + +TEST(CamTest, Green) { + Cam cam = CamFromInt(GREEN); + + EXPECT_THAT(cam.hue, DoubleNear(142.139, 0.001)); + EXPECT_THAT(cam.chroma, DoubleNear(108.410, 0.001)); + EXPECT_THAT(cam.j, DoubleNear(79.331, 0.001)); + EXPECT_THAT(cam.m, DoubleNear(85.587, 0.001)); + EXPECT_THAT(cam.s, DoubleNear(78.604, 0.001)); + EXPECT_THAT(cam.q, DoubleNear(138.520, 0.001)); +} + +TEST(CamTest, Blue) { + Cam cam = CamFromInt(BLUE); + + EXPECT_THAT(cam.hue, DoubleNear(282.788, 0.001)); + EXPECT_THAT(cam.chroma, DoubleNear(87.230, 0.001)); + EXPECT_THAT(cam.j, DoubleNear(25.465, 0.001)); + EXPECT_THAT(cam.m, DoubleNear(68.867, 0.001)); + EXPECT_THAT(cam.s, DoubleNear(93.674, 0.001)); + EXPECT_THAT(cam.q, DoubleNear(78.481, 0.001)); +} + +TEST(CamTest, White) { + Cam cam = CamFromInt(WHITE); + + EXPECT_THAT(cam.hue, DoubleNear(209.492, 0.001)); + EXPECT_THAT(cam.chroma, DoubleNear(2.869, 0.001)); + EXPECT_THAT(cam.j, DoubleNear(100.0, 0.001)); + EXPECT_THAT(cam.m, DoubleNear(2.265, 0.001)); + EXPECT_THAT(cam.s, DoubleNear(12.068, 0.001)); + EXPECT_THAT(cam.q, DoubleNear(155.521, 0.001)); +} + +TEST(CamTest, Black) { + Cam cam = CamFromInt(BLACK); + + EXPECT_THAT(cam.hue, DoubleNear(0.0, 0.001)); + EXPECT_THAT(cam.chroma, DoubleNear(0.0, 0.001)); + EXPECT_THAT(cam.j, DoubleNear(0.0, 0.001)); + EXPECT_THAT(cam.m, DoubleNear(0.0, 0.001)); + EXPECT_THAT(cam.s, DoubleNear(0.0, 0.001)); + EXPECT_THAT(cam.q, DoubleNear(0.0, 0.001)); +} + +TEST(CamTest, RedRoundTrip) { + Cam cam = CamFromInt(RED); + Argb argb = IntFromCam(cam); + EXPECT_THAT(argb, Eq(RED)); +} + +TEST(CamTest, GreenRoundTrip) { + Cam cam = CamFromInt(GREEN); + Argb argb = IntFromCam(cam); + EXPECT_THAT(argb, Eq(GREEN)); +} + +TEST(CamTest, BlueRoundTrip) { + Cam cam = CamFromInt(BLUE); + Argb argb = IntFromCam(cam); + EXPECT_THAT(argb, Eq(BLUE)); +} +} // namespace + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/hct.cc b/third_party/material-color/cpp/cam/hct.cc new file mode 100644 index 0000000..d165ca0 --- /dev/null +++ b/third_party/material-color/cpp/cam/hct.cc @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/hct.h" + +#include "cpp/cam/hct_solver.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { +Hct::Hct(double hue, double chroma, double tone) { + SetInternalState(SolveToInt(hue, chroma, tone)); +} + +Hct::Hct(Argb argb) { SetInternalState(argb); } + +double Hct::get_hue() const { return hue_; } + +double Hct::get_chroma() const { return chroma_; } + +double Hct::get_tone() const { return tone_; } + +Argb Hct::ToInt() const { return argb_; } + +void Hct::set_hue(double new_hue) { + SetInternalState(SolveToInt(new_hue, chroma_, tone_)); +} + +void Hct::set_chroma(double new_chroma) { + SetInternalState(SolveToInt(hue_, new_chroma, tone_)); +} + +void Hct::set_tone(double new_tone) { + SetInternalState(SolveToInt(hue_, chroma_, new_tone)); +} + +void Hct::SetInternalState(Argb argb) { + argb_ = argb; + Cam cam = CamFromInt(argb); + hue_ = cam.hue; + chroma_ = cam.chroma; + tone_ = LstarFromArgb(argb); +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/hct.h b/third_party/material-color/cpp/cam/hct.h new file mode 100644 index 0000000..e8e02f3 --- /dev/null +++ b/third_party/material-color/cpp/cam/hct.h @@ -0,0 +1,137 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_CAM_HCT_H_ +#define CPP_CAM_HCT_H_ + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +/** + * HCT: hue, chroma, and tone. + * + * A color system built using CAM16 hue and chroma, and L* (lightness) from + * the L*a*b* color space, providing a perceptually accurate + * color measurement system that can also accurately render what colors + * will appear as in different lighting environments. + * + * Using L* creates a link between the color system, contrast, and thus + * accessibility. Contrast ratio depends on relative luminance, or Y in the XYZ + * color space. L*, or perceptual luminance can be calculated from Y. + * + * Unlike Y, L* is linear to human perception, allowing trivial creation of + * accurate color tones. + * + * Unlike contrast ratio, measuring contrast in L* is linear, and simple to + * calculate. A difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, + * and a difference of 50 guarantees a contrast ratio >= 4.5. + */ +class Hct { + public: + /** + * Creates an HCT color from hue, chroma, and tone. + * + * @param hue 0 <= hue < 360; invalid values are corrected. + * @param chroma >= 0; the maximum value of chroma depends on the hue + * and tone. May be lower than the requested chroma. + * @param tone 0 <= tone <= 100; invalid values are corrected. + * @return HCT representation of a color in default viewing conditions. + */ + Hct(double hue, double chroma, double tone); + + /** + * Creates an HCT color from a color. + * + * @param argb ARGB representation of a color. + * @return HCT representation of a color in default viewing conditions + */ + explicit Hct(Argb argb); + + /** + * Returns the hue of the color. + * + * @return hue of the color, in degrees. + */ + double get_hue() const; + + /** + * Returns the chroma of the color. + * + * @return chroma of the color. + */ + double get_chroma() const; + + /** + * Returns the tone of the color. + * + * @return tone of the color, satisfying 0 <= tone <= 100. + */ + double get_tone() const; + + /** + * Returns the color in ARGB format. + * + * @return an integer, representing the color in ARGB format. + */ + Argb ToInt() const; + + /** + * Sets the hue of this color. Chroma may decrease because chroma has a + * different maximum for any given hue and tone. + * + * @param new_hue 0 <= new_hue < 360; invalid values are corrected. + */ + void set_hue(double new_hue); + + /** + * Sets the chroma of this color. Chroma may decrease because chroma has a + * different maximum for any given hue and tone. + * + * @param new_chroma 0 <= new_chroma < ? + */ + void set_chroma(double new_chroma); + + /** + * Sets the tone of this color. Chroma may decrease because chroma has a + * different maximum for any given hue and tone. + * + * @param new_tone 0 <= new_tone <= 100; invalid valids are corrected. + */ + void set_tone(double new_tone); + + /** + * For using HCT as a key in a ordered map. + */ + bool operator<(const Hct& a) const { return hue_ < a.hue_; } + + private: + /** + * Sets the Hct object to represent an sRGB color. + * + * @param argb the new color as an integer in ARGB format. + */ + void SetInternalState(Argb argb); + + double hue_ = 0.0; + double chroma_ = 0.0; + double tone_ = 0.0; + Argb argb_ = 0; +}; + +} // namespace material_color_utilities + +#endif // CPP_CAM_HCT_H_ diff --git a/third_party/material-color/cpp/cam/hct_solver.cc b/third_party/material-color/cpp/cam/hct_solver.cc new file mode 100644 index 0000000..fcf15ce --- /dev/null +++ b/third_party/material-color/cpp/cam/hct_solver.cc @@ -0,0 +1,526 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/hct_solver.h" + +#include + +#include "cpp/cam/viewing_conditions.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +constexpr double kScaledDiscountFromLinrgb[3][3] = { + { + 0.001200833568784504, + 0.002389694492170889, + 0.0002795742885861124, + }, + { + 0.0005891086651375999, + 0.0029785502573438758, + 0.0003270666104008398, + }, + { + 0.00010146692491640572, + 0.0005364214359186694, + 0.0032979401770712076, + }, +}; + +constexpr double kLinrgbFromScaledDiscount[3][3] = { + { + 1373.2198709594231, + -1100.4251190754821, + -7.278681089101213, + }, + { + -271.815969077903, + 559.6580465940733, + -32.46047482791194, + }, + { + 1.9622899599665666, + -57.173814538844006, + 308.7233197812385, + }, +}; + +constexpr double kYFromLinrgb[3] = {0.2126, 0.7152, 0.0722}; + +constexpr double kCriticalPlanes[255] = { + 0.015176349177441876, 0.045529047532325624, 0.07588174588720938, + 0.10623444424209313, 0.13658714259697685, 0.16693984095186062, + 0.19729253930674434, 0.2276452376616281, 0.2579979360165119, + 0.28835063437139563, 0.3188300904430532, 0.350925934958123, + 0.3848314933096426, 0.42057480301049466, 0.458183274052838, + 0.4976837250274023, 0.5391024159806381, 0.5824650784040898, + 0.6277969426914107, 0.6751227633498623, 0.7244668422128921, + 0.775853049866786, 0.829304845476233, 0.8848452951698498, + 0.942497089126609, 1.0022825574869039, 1.0642236851973577, + 1.1283421258858297, 1.1946592148522128, 1.2631959812511864, + 1.3339731595349034, 1.407011200216447, 1.4823302800086415, + 1.5599503113873272, 1.6398909516233677, 1.7221716113234105, + 1.8068114625156377, 1.8938294463134073, 1.9832442801866852, + 2.075074464868551, 2.1693382909216234, 2.2660538449872063, + 2.36523901573795, 2.4669114995532007, 2.5710888059345764, + 2.6777882626779785, 2.7870270208169257, 2.898822059350997, + 3.0131901897720907, 3.1301480604002863, 3.2497121605402226, + 3.3718988244681087, 3.4967242352587946, 3.624204428461639, + 3.754355295633311, 3.887192587735158, 4.022731918402185, + 4.160988767090289, 4.301978482107941, 4.445716283538092, + 4.592217266055746, 4.741496401646282, 4.893568542229298, + 5.048448422192488, 5.20615066083972, 5.3666897647573375, + 5.5300801301023865, 5.696336044816294, 5.865471690767354, + 6.037501145825082, 6.212438385869475, 6.390297286737924, + 6.571091626112461, 6.7548350853498045, 6.941541251256611, + 7.131223617812143, 7.323895587840543, 7.5195704746346665, + 7.7182615035334345, 7.919981813454504, 8.124744458384042, + 8.332562408825165, 8.543448553206703, 8.757415699253682, + 8.974476575321063, 9.194643831691977, 9.417930041841839, + 9.644347703669503, 9.873909240696694, 10.106627003236781, + 10.342513269534024, 10.58158024687427, 10.8238400726681, + 11.069304815507364, 11.317986476196008, 11.569896988756009, + 11.825048221409341, 12.083451977536606, 12.345119996613247, + 12.610063955123938, 12.878295467455942, 13.149826086772048, + 13.42466730586372, 13.702830557985108, 13.984327217668513, + 14.269168601521828, 14.55736596900856, 14.848930523210871, + 15.143873411576273, 15.44220572664832, 15.743938506781891, + 16.04908273684337, 16.35764934889634, 16.66964922287304, + 16.985093187232053, 17.30399201960269, 17.62635644741625, + 17.95219714852476, 18.281524751807332, 18.614349837764564, + 18.95068293910138, 19.290534541298456, 19.633915083172692, + 19.98083495742689, 20.331304511189067, 20.685334046541502, + 21.042933821039977, 21.404114048223256, 21.76888489811322, + 22.137256497705877, 22.50923893145328, 22.884842241736916, + 23.264076429332462, 23.6469514538663, 24.033477234264016, + 24.42366364919083, 24.817520537484558, 25.21505769858089, + 25.61628489293138, 26.021211842414342, 26.429848230738664, + 26.842203703840827, 27.258287870275353, 27.678110301598522, + 28.10168053274597, 28.529008062403893, 28.96010235337422, + 29.39497283293396, 29.83362889318845, 30.276079891419332, + 30.722335150426627, 31.172403958865512, 31.62629557157785, + 32.08401920991837, 32.54558406207592, 33.010999283389665, + 33.4802739966603, 33.953417292456834, 34.430438229418264, + 34.911345834551085, 35.39614910352207, 35.88485700094671, + 36.37747846067349, 36.87402238606382, 37.37449765026789, + 37.87891309649659, 38.38727753828926, 38.89959975977785, + 39.41588851594697, 39.93615253289054, 40.460400508064545, + 40.98864111053629, 41.520882981230194, 42.05713473317016, + 42.597404951718396, 43.141702194811224, 43.6900349931913, + 44.24241185063697, 44.798841244188324, 45.35933162437017, + 45.92389141541209, 46.49252901546552, 47.065252796817916, + 47.64207110610409, 48.22299226451468, 48.808024568002054, + 49.3971762874833, 49.9904556690408, 50.587870934119984, + 51.189430279724725, 51.79514187861014, 52.40501387947288, + 53.0190544071392, 53.637271562750364, 54.259673423945976, + 54.88626804504493, 55.517063457223934, 56.15206766869424, + 56.79128866487574, 57.43473440856916, 58.08241284012621, + 58.734331877617365, 59.39049941699807, 60.05092333227251, + 60.715611475655585, 61.38457167773311, 62.057811747619894, + 62.7353394731159, 63.417162620860914, 64.10328893648692, + 64.79372614476921, 65.48848194977529, 66.18756403501224, + 66.89098006357258, 67.59873767827808, 68.31084450182222, + 69.02730813691093, 69.74813616640164, 70.47333615344107, + 71.20291564160104, 71.93688215501312, 72.67524319850172, + 73.41800625771542, 74.16517879925733, 74.9167682708136, + 75.67278210128072, 76.43322770089146, 77.1981124613393, + 77.96744375590167, 78.74122893956174, 79.51947534912904, + 80.30219030335869, 81.08938110306934, 81.88105503125999, + 82.67721935322541, 83.4778813166706, 84.28304815182372, + 85.09272707154808, 85.90692527145302, 86.72564993000343, + 87.54890820862819, 88.3767072518277, 89.2090541872801, + 90.04595612594655, 90.88742016217518, 91.73345337380438, + 92.58406282226491, 93.43925555268066, 94.29903859396902, + 95.16341895893969, 96.03240364439274, 96.9059996312159, + 97.78421388448044, 98.6670533535366, 99.55452497210776, +}; + +/** + * Sanitizes a small enough angle in radians. + * + * @param angle An angle in radians; must not deviate too much from 0. + * @return A coterminal angle between 0 and 2pi. + */ +double SanitizeRadians(double angle) { return fmod(angle + kPi * 8, kPi * 2); } + +/** + * Delinearizes an RGB component, returning a floating-point number. + * + * @param rgb_component 0.0 <= rgb_component <= 100.0, represents linear R/G/B + * channel + * @return 0.0 <= output <= 255.0, color channel converted to regular RGB space + */ +double TrueDelinearized(double rgb_component) { + double normalized = rgb_component / 100.0; + double delinearized = 0.0; + if (normalized <= 0.0031308) { + delinearized = normalized * 12.92; + } else { + delinearized = 1.055 * pow(normalized, 1.0 / 2.4) - 0.055; + } + return delinearized * 255.0; +} + +double ChromaticAdaptation(double component) { + double af = pow(abs(component), 0.42); + return Signum(component) * 400.0 * af / (af + 27.13); +} + +/** + * Returns the hue of a linear RGB color in CAM16. + * + * @param linrgb The linear RGB coordinates of a color. + * @return The hue of the color in CAM16, in radians. + */ +double HueOf(Vec3 linrgb) { + Vec3 scaledDiscount = MatrixMultiply(linrgb, kScaledDiscountFromLinrgb); + double r_a = ChromaticAdaptation(scaledDiscount.a); + double g_a = ChromaticAdaptation(scaledDiscount.b); + double b_a = ChromaticAdaptation(scaledDiscount.c); + // redness-greenness + double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0; + // yellowness-blueness + double b = (r_a + g_a - 2.0 * b_a) / 9.0; + return atan2(b, a); +} + +bool AreInCyclicOrder(double a, double b, double c) { + double delta_a_b = SanitizeRadians(b - a); + double delta_a_c = SanitizeRadians(c - a); + return delta_a_b < delta_a_c; +} + +/** + * Solves the lerp equation. + * + * @param source The starting number. + * @param mid The number in the middle. + * @param target The ending number. + * @return A number t such that lerp(source, target, t) = mid. + */ +double Intercept(double source, double mid, double target) { + return (mid - source) / (target - source); +} + +Vec3 LerpPoint(Vec3 source, double t, Vec3 target) { + return (Vec3){ + source.a + (target.a - source.a) * t, + source.b + (target.b - source.b) * t, + source.c + (target.c - source.c) * t, + }; +} + +double GetAxis(Vec3 vector, int axis) { + switch (axis) { + case 0: + return vector.a; + case 1: + return vector.b; + case 2: + return vector.c; + default: + return -1.0; + } +} + +/** + * Intersects a segment with a plane. + * + * @param source The coordinates of point A. + * @param coordinate The R-, G-, or B-coordinate of the plane. + * @param target The coordinates of point B. + * @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B) + * @return The intersection point of the segment AB with the plane R=coordinate, + * G=coordinate, or B=coordinate + */ +Vec3 SetCoordinate(Vec3 source, double coordinate, Vec3 target, int axis) { + double t = + Intercept(GetAxis(source, axis), coordinate, GetAxis(target, axis)); + return LerpPoint(source, t, target); +} + +bool IsBounded(double x) { return 0.0 <= x && x <= 100.0; } + +/** + * Returns the nth possible vertex of the polygonal intersection. + * + * @param y The Y value of the plane. + * @param n The zero-based index of the point. 0 <= n <= 11. + * @return The nth possible vertex of the polygonal intersection of the y plane + * and the RGB cube, in linear RGB coordinates, if it exists. If this possible + * vertex lies outside of the cube, + * [-1.0, -1.0, -1.0] is returned. + */ +Vec3 NthVertex(double y, int n) { + double k_r = kYFromLinrgb[0]; + double k_g = kYFromLinrgb[1]; + double k_b = kYFromLinrgb[2]; + double coord_a = n % 4 <= 1 ? 0.0 : 100.0; + double coord_b = n % 2 == 0 ? 0.0 : 100.0; + if (n < 4) { + double g = coord_a; + double b = coord_b; + double r = (y - g * k_g - b * k_b) / k_r; + if (IsBounded(r)) { + return (Vec3){r, g, b}; + } else { + return (Vec3){-1.0, -1.0, -1.0}; + } + } else if (n < 8) { + double b = coord_a; + double r = coord_b; + double g = (y - r * k_r - b * k_b) / k_g; + if (IsBounded(g)) { + return (Vec3){r, g, b}; + } else { + return (Vec3){-1.0, -1.0, -1.0}; + } + } else { + double r = coord_a; + double g = coord_b; + double b = (y - r * k_r - g * k_g) / k_b; + if (IsBounded(b)) { + return (Vec3){r, g, b}; + } else { + return (Vec3){-1.0, -1.0, -1.0}; + } + } +} + +/** + * Finds the segment containing the desired color. + * + * @param y The Y value of the color. + * @param target_hue The hue of the color. + * @return A list of two sets of linear RGB coordinates, each corresponding to + * an endpoint of the segment containing the desired color. + */ +void BisectToSegment(double y, double target_hue, Vec3 out[2]) { + Vec3 left = (Vec3){-1.0, -1.0, -1.0}; + Vec3 right = left; + double left_hue = 0.0; + double right_hue = 0.0; + bool initialized = false; + bool uncut = true; + for (int n = 0; n < 12; n++) { + Vec3 mid = NthVertex(y, n); + if (mid.a < 0) { + continue; + } + double mid_hue = HueOf(mid); + if (!initialized) { + left = mid; + right = mid; + left_hue = mid_hue; + right_hue = mid_hue; + initialized = true; + continue; + } + if (uncut || AreInCyclicOrder(left_hue, mid_hue, right_hue)) { + uncut = false; + if (AreInCyclicOrder(left_hue, target_hue, mid_hue)) { + right = mid; + right_hue = mid_hue; + } else { + left = mid; + left_hue = mid_hue; + } + } + } + out[0] = left; + out[1] = right; +} + +Vec3 Midpoint(Vec3 a, Vec3 b) { + return (Vec3){ + (a.a + b.a) / 2, + (a.b + b.b) / 2, + (a.c + b.c) / 2, + }; +} + +int CriticalPlaneBelow(double x) { return (int)floor(x - 0.5); } + +int CriticalPlaneAbove(double x) { return (int)ceil(x - 0.5); } + +/** + * Finds a color with the given Y and hue on the boundary of the cube. + * + * @param y The Y value of the color. + * @param target_hue The hue of the color. + * @return The desired color, in linear RGB coordinates. + */ +Vec3 BisectToLimit(double y, double target_hue) { + Vec3 segment[2]; + BisectToSegment(y, target_hue, segment); + Vec3 left = segment[0]; + double left_hue = HueOf(left); + Vec3 right = segment[1]; + for (int axis = 0; axis < 3; axis++) { + if (GetAxis(left, axis) != GetAxis(right, axis)) { + int l_plane = -1; + int r_plane = 255; + if (GetAxis(left, axis) < GetAxis(right, axis)) { + l_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(left, axis))); + r_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(right, axis))); + } else { + l_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(left, axis))); + r_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(right, axis))); + } + for (int i = 0; i < 8; i++) { + if (abs(r_plane - l_plane) <= 1) { + break; + } else { + int m_plane = (int)floor((l_plane + r_plane) / 2.0); + double mid_plane_coordinate = kCriticalPlanes[m_plane]; + Vec3 mid = SetCoordinate(left, mid_plane_coordinate, right, axis); + double mid_hue = HueOf(mid); + if (AreInCyclicOrder(left_hue, target_hue, mid_hue)) { + right = mid; + r_plane = m_plane; + } else { + left = mid; + left_hue = mid_hue; + l_plane = m_plane; + } + } + } + } + } + return Midpoint(left, right); +} + +double InverseChromaticAdaptation(double adapted) { + double adapted_abs = abs(adapted); + double base = fmax(0, 27.13 * adapted_abs / (400.0 - adapted_abs)); + return Signum(adapted) * pow(base, 1.0 / 0.42); +} + +/** + * Finds a color with the given hue, chroma, and Y. + * + * @param hue_radians The desired hue in radians. + * @param chroma The desired chroma. + * @param y The desired Y. + * @return The desired color as a hexadecimal integer, if found; 0 otherwise. + */ +Argb FindResultByJ(double hue_radians, double chroma, double y) { + // Initial estimate of j. + double j = sqrt(y) * 11.0; + // =========================================================== + // Operations inlined from Cam16 to avoid repeated calculation + // =========================================================== + ViewingConditions viewing_conditions = kDefaultViewingConditions; + double t_inner_coeff = + 1 / + pow(1.64 - pow(0.29, viewing_conditions.background_y_to_white_point_y), + 0.73); + double e_hue = 0.25 * (cos(hue_radians + 2.0) + 3.8); + double p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.n_c * + viewing_conditions.ncb; + double h_sin = sin(hue_radians); + double h_cos = cos(hue_radians); + for (int iteration_round = 0; iteration_round < 5; iteration_round++) { + // =========================================================== + // Operations inlined from Cam16 to avoid repeated calculation + // =========================================================== + double j_normalized = j / 100.0; + double alpha = + chroma == 0.0 || j == 0.0 ? 0.0 : chroma / sqrt(j_normalized); + double t = pow(alpha * t_inner_coeff, 1.0 / 0.9); + double ac = + viewing_conditions.aw * + pow(j_normalized, 1.0 / viewing_conditions.c / viewing_conditions.z); + double p2 = ac / viewing_conditions.nbb; + double gamma = 23.0 * (p2 + 0.305) * t / + (23.0 * p1 + 11 * t * h_cos + 108.0 * t * h_sin); + double a = gamma * h_cos; + double b = gamma * h_sin; + double r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0; + double g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0; + double b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0; + double r_c_scaled = InverseChromaticAdaptation(r_a); + double g_c_scaled = InverseChromaticAdaptation(g_a); + double b_c_scaled = InverseChromaticAdaptation(b_a); + Vec3 scaled = (Vec3){r_c_scaled, g_c_scaled, b_c_scaled}; + Vec3 linrgb = MatrixMultiply(scaled, kLinrgbFromScaledDiscount); + // =========================================================== + // Operations inlined from Cam16 to avoid repeated calculation + // =========================================================== + if (linrgb.a < 0 || linrgb.b < 0 || linrgb.c < 0) { + return 0; + } + double k_r = kYFromLinrgb[0]; + double k_g = kYFromLinrgb[1]; + double k_b = kYFromLinrgb[2]; + double fnj = k_r * linrgb.a + k_g * linrgb.b + k_b * linrgb.c; + if (fnj <= 0) { + return 0; + } + if (iteration_round == 4 || abs(fnj - y) < 0.002) { + if (linrgb.a > 100.01 || linrgb.b > 100.01 || linrgb.c > 100.01) { + return 0; + } + return ArgbFromLinrgb(linrgb); + } + // Iterates with Newton method, + // Using 2 * fn(j) / j as the approximation of fn'(j) + j = j - (fnj - y) * j / (2 * fnj); + } + return 0; +} + +/** + * Finds an sRGB color with the given hue, chroma, and L*, if possible. + * + * @param hue_degrees The desired hue, in degrees. + * @param chroma The desired chroma. + * @param lstar The desired L*. + * @return A hexadecimal representing the sRGB color. The color has sufficiently + * close hue, chroma, and L* to the desired values, if possible; otherwise, the + * hue and L* will be sufficiently close, and chroma will be maximized. + */ +Argb SolveToInt(double hue_degrees, double chroma, double lstar) { + if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) { + return IntFromLstar(lstar); + } + hue_degrees = SanitizeDegreesDouble(hue_degrees); + double hue_radians = hue_degrees / 180 * kPi; + double y = YFromLstar(lstar); + Argb exact_answer = FindResultByJ(hue_radians, chroma, y); + if (exact_answer != 0) { + return exact_answer; + } + Vec3 linrgb = BisectToLimit(y, hue_radians); + return ArgbFromLinrgb(linrgb); +} + +/** + * Finds an sRGB color with the given hue, chroma, and L*, if possible. + * + * @param hue_degrees The desired hue, in degrees. + * @param chroma The desired chroma. + * @param lstar The desired L*. + * @return An CAM16 object representing the sRGB color. The color has + * sufficiently close hue, chroma, and L* to the desired values, if possible; + * otherwise, the hue and L* will be sufficiently close, and chroma will be + * maximized. + */ +Cam SolveToCam(double hue_degrees, double chroma, double lstar) { + return CamFromInt(SolveToInt(hue_degrees, chroma, lstar)); +} +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/hct_solver.h b/third_party/material-color/cpp/cam/hct_solver.h new file mode 100644 index 0000000..b426af8 --- /dev/null +++ b/third_party/material-color/cpp/cam/hct_solver.h @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_CAM_HCT_SOLVER_H_ +#define CPP_CAM_HCT_SOLVER_H_ + +#include "cpp/cam/cam.h" + +namespace material_color_utilities { + +Argb SolveToInt(double hue_degrees, double chroma, double lstar); +Cam SolveToCam(double hue_degrees, double chroma, double lstar); + +} // namespace material_color_utilities +#endif // CPP_CAM_HCT_SOLVER_H_ diff --git a/third_party/material-color/cpp/cam/hct_solver_test.cc b/third_party/material-color/cpp/cam/hct_solver_test.cc new file mode 100644 index 0000000..fbaa2e7 --- /dev/null +++ b/third_party/material-color/cpp/cam/hct_solver_test.cc @@ -0,0 +1,74 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/hct_solver.h" + +#include "testing/base/public/gmock.h" +#include "testing/base/public/gunit.h" +#include "cpp/cam/cam.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { +using testing::Eq; + +TEST(HctSolverTest, Red) { + // Compute HCT + Argb color = 0xFFFE0315; + Cam cam = CamFromInt(color); + double tone = LstarFromArgb(color); + + // Compute input + Argb recovered = SolveToInt(cam.hue, cam.chroma, tone); + EXPECT_THAT(recovered, Eq(color)); +} + +TEST(HctSolverTest, Green) { + // Compute HCT + Argb color = 0xFF15FE03; + Cam cam = CamFromInt(color); + double tone = LstarFromArgb(color); + + // Compute input + Argb recovered = SolveToInt(cam.hue, cam.chroma, tone); + EXPECT_THAT(recovered, Eq(color)); +} + +TEST(HctSolverTest, Blue) { + // Compute HCT + Argb color = 0xFF0315FE; + Cam cam = CamFromInt(color); + double tone = LstarFromArgb(color); + + // Compute input + Argb recovered = SolveToInt(cam.hue, cam.chroma, tone); + EXPECT_THAT(recovered, Eq(color)); +} + +TEST(HctSolverTest, Exhaustive) { + for (int colorIndex = 0; colorIndex <= 0xFFFFFF; colorIndex++) { + Argb color = 0xFF000000 | colorIndex; + Cam cam = CamFromInt(color); + double tone = LstarFromArgb(color); + + // Compute input + Argb recovered = SolveToInt(cam.hue, cam.chroma, tone); + EXPECT_THAT(recovered, Eq(color)); + } +} +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/hct_test.cc b/third_party/material-color/cpp/cam/hct_test.cc new file mode 100644 index 0000000..b40e818 --- /dev/null +++ b/third_party/material-color/cpp/cam/hct_test.cc @@ -0,0 +1,100 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/hct.h" + +#include + +#include "testing/base/public/gmock.h" +#include "testing/base/public/gunit.h" +#include "cpp/cam/cam.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { +using ::testing::Combine; +using ::testing::DoubleNear; +using ::testing::Eq; +using ::testing::Lt; +using ::testing::TestWithParam; +using ::testing::Values; + +TEST(HctTest, LimitedToSRGB) { + // Ensures that the HCT class can only represent sRGB colors. + // An impossibly high chroma is used. + Hct hct(/*hue=*/120.0, /*chroma=*/200.0, /*tone=*/50.0); + Argb argb = hct.ToInt(); + + // The hue, chroma, and tone members of hct should actually + // represent the sRGB color. + EXPECT_THAT(CamFromInt(argb).hue, Eq(hct.get_hue())); + EXPECT_THAT(CamFromInt(argb).chroma, Eq(hct.get_chroma())); + EXPECT_THAT(LstarFromArgb(argb), Eq(hct.get_tone())); +} + +TEST(HctTest, TruncatesColors) { + // Ensures that HCT truncates colors. + Hct hct(/*hue=*/120.0, /*chroma=*/60.0, /*tone=*/50.0); + double chroma = hct.get_chroma(); + EXPECT_THAT(chroma, Lt(60.0)); + + // The new chroma should be lower than the original. + hct.set_tone(180.0); + EXPECT_THAT(hct.get_chroma(), Lt(chroma)); +} + +bool IsOnBoundary(int rgb_component) { + return rgb_component == 0 || rgb_component == 255; +} + +bool ColorIsOnBoundary(int argb) { + return IsOnBoundary(RedFromInt(argb)) || IsOnBoundary(GreenFromInt(argb)) || + IsOnBoundary(BlueFromInt(argb)); +} + +using HctTest = TestWithParam>; + +TEST_P(HctTest, Correctness) { + std::tuple hctTuple = GetParam(); + int hue = std::get<0>(hctTuple); + int chroma = std::get<1>(hctTuple); + int tone = std::get<2>(hctTuple); + + Hct color(hue, chroma, tone); + + if (chroma > 0) { + EXPECT_THAT(color.get_hue(), DoubleNear(hue, 4.0)); + } + + EXPECT_THAT(color.get_chroma(), Lt(chroma + 2.5)); + + if (color.get_chroma() < chroma - 2.5) { + EXPECT_TRUE(ColorIsOnBoundary(color.ToInt())); + } + + EXPECT_THAT(color.get_tone(), DoubleNear(tone, 0.5)); +} + +INSTANTIATE_TEST_SUITE_P( + HctTests, HctTest, + Combine(/*hues*/ Values(15, 45, 75, 105, 135, 165, 195, 225, 255, 285, 315, + 345), + /*chromas*/ Values(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100), + /*tones*/ Values(20, 30, 40, 50, 60, 70, 80))); + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/viewing_conditions.cc b/third_party/material-color/cpp/cam/viewing_conditions.cc new file mode 100644 index 0000000..28c5763 --- /dev/null +++ b/third_party/material-color/cpp/cam/viewing_conditions.cc @@ -0,0 +1,118 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/cam/viewing_conditions.h" + +#include +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +static double lerp(double start, double stop, double amount) { + return (1.0 - amount) * start + amount * stop; +} + +ViewingConditions CreateViewingConditions(const double white_point[3], + const double adapting_luminance, + const double background_lstar, + const double surround, + const bool discounting_illuminant) { + double background_lstar_corrected = + (background_lstar < 30.0) ? 30.0 : background_lstar; + double rgb_w[3] = { + 0.401288 * white_point[0] + 0.650173 * white_point[1] - + 0.051461 * white_point[2], + -0.250268 * white_point[0] + 1.204414 * white_point[1] + + 0.045854 * white_point[2], + -0.002079 * white_point[0] + 0.048952 * white_point[1] + + 0.953127 * white_point[2], + }; + double f = 0.8 + (surround / 10.0); + double c = f >= 0.9 ? lerp(0.59, 0.69, (f - 0.9) * 10.0) + : lerp(0.525, 0.59, (f - 0.8) * 10.0); + double d = discounting_illuminant + ? 1.0 + : f * (1.0 - ((1.0 / 3.6) * + exp((-adapting_luminance - 42.0) / 92.0))); + d = d > 1.0 ? 1.0 : d < 0.0 ? 0.0 : d; + double nc = f; + double rgb_d[3] = {(d * (100.0 / rgb_w[0]) + 1.0 - d), + (d * (100.0 / rgb_w[1]) + 1.0 - d), + (d * (100.0 / rgb_w[2]) + 1.0 - d)}; + + double k = 1.0 / (5.0 * adapting_luminance + 1.0); + double k4 = k * k * k * k; + double k4f = 1.0 - k4; + double fl = (k4 * adapting_luminance) + + (0.1 * k4f * k4f * pow(5.0 * adapting_luminance, 1.0 / 3.0)); + double fl_root = pow(fl, 0.25); + double n = YFromLstar(background_lstar_corrected) / white_point[1]; + double z = 1.48 + sqrt(n); + double nbb = 0.725 / pow(n, 0.2); + double ncb = nbb; + double rgb_a_factors[3] = {pow(fl * rgb_d[0] * rgb_w[0] / 100.0, 0.42), + pow(fl * rgb_d[1] * rgb_w[1] / 100.0, 0.42), + pow(fl * rgb_d[2] * rgb_w[2] / 100.0, 0.42)}; + double rgb_a[3] = { + 400.0 * rgb_a_factors[0] / (rgb_a_factors[0] + 27.13), + 400.0 * rgb_a_factors[1] / (rgb_a_factors[1] + 27.13), + 400.0 * rgb_a_factors[2] / (rgb_a_factors[2] + 27.13), + }; + double aw = (40.0 * rgb_a[0] + 20.0 * rgb_a[1] + rgb_a[2]) / 20.0 * nbb; + ViewingConditions viewingConditions = { + adapting_luminance, + background_lstar_corrected, + surround, + discounting_illuminant, + n, + aw, + nbb, + ncb, + c, + nc, + fl, + fl_root, + z, + {white_point[0], white_point[1], white_point[2]}, + {rgb_d[0], rgb_d[1], rgb_d[2]}, + }; + return viewingConditions; +} + +ViewingConditions DefaultWithBackgroundLstar(const double background_lstar) { + return CreateViewingConditions(kWhitePointD65, + (200.0 / kPi * YFromLstar(50.0) / 100.0), + background_lstar, 2.0, 0); +} + +void PrintDefaultFrame() { + ViewingConditions frame = CreateViewingConditions( + kWhitePointD65, (200.0 / kPi * YFromLstar(50.0) / 100.0), 50.0, 2.0, 0); + printf( + "(Frame){%0.9lf,\n %0.9lf,\n %0.9lf,\n %s\n, %0.9lf,\n " + "%0.9lf,\n%0.9lf,\n%0.9lf,\n%0.9lf,\n%0.9lf,\n" + "%0.9lf,\n%0.9lf,\n%0.9lf,\n%0.9lf,\n" + "%0.9lf,\n%0.9lf\n};", + frame.adapting_luminance, frame.background_lstar, frame.surround, + frame.discounting_illuminant ? "true" : "false", + frame.background_y_to_white_point_y, frame.aw, frame.nbb, frame.ncb, + frame.c, frame.n_c, frame.fl, frame.fl_root, frame.z, frame.rgb_d[0], + frame.rgb_d[1], frame.rgb_d[2]); +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/cam/viewing_conditions.h b/third_party/material-color/cpp/cam/viewing_conditions.h new file mode 100644 index 0000000..5864379 --- /dev/null +++ b/third_party/material-color/cpp/cam/viewing_conditions.h @@ -0,0 +1,68 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_CAM_VIEWING_CONDITIONS_H_ +#define CPP_CAM_VIEWING_CONDITIONS_H_ + +namespace material_color_utilities { + +struct ViewingConditions { + double adapting_luminance = 0.0; + double background_lstar = 0.0; + double surround = 0.0; + bool discounting_illuminant = false; + double background_y_to_white_point_y = 0.0; + double aw = 0.0; + double nbb = 0.0; + double ncb = 0.0; + double c = 0.0; + double n_c = 0.0; + double fl = 0.0; + double fl_root = 0.0; + double z = 0.0; + + double white_point[3] = {0.0, 0.0, 0.0}; + double rgb_d[3] = {0.0, 0.0, 0.0}; +}; + +ViewingConditions CreateViewingConditions(const double white_point[3], + const double adapting_luminance, + const double background_lstar, + const double surround, + const bool discounting_illuminant); + +ViewingConditions DefaultWithBackgroundLstar(const double background_lstar); + +static const ViewingConditions kDefaultViewingConditions = (ViewingConditions){ + 11.725676537, + 50.000000000, + 2.000000000, + false, + 0.184186503, + 29.981000900, + 1.016919255, + 1.016919255, + 0.689999998, + 1.000000000, + 0.388481468, + 0.789482653, + 1.909169555, + {95.047, 100.0, 108.883}, + {1.021177769, 0.986307740, 0.933960497}, +}; + +} // namespace material_color_utilities +#endif // CPP_CAM_VIEWING_CONDITIONS_H_ diff --git a/third_party/material-color/cpp/contrast/contrast.cc b/third_party/material-color/cpp/contrast/contrast.cc new file mode 100644 index 0000000..c94d444 --- /dev/null +++ b/third_party/material-color/cpp/contrast/contrast.cc @@ -0,0 +1,138 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/contrast/contrast.h" + +#include +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { +// Given a color and a contrast ratio to reach, the luminance of a color that +// reaches that ratio with the color can be calculated. However, that luminance +// may not contrast as desired, i.e. the contrast ratio of the input color +// and the returned luminance may not reach the contrast ratio asked for. +// +// When the desired contrast ratio and the result contrast ratio differ by +// more than this amount, an error value should be returned, or the method +// should be documented as 'unsafe', meaning, it will return a valid luminance +// but that luminance may not meet the requested contrast ratio. +// +// 0.04 selected because it ensures the resulting ratio rounds to the +// same tenth. +constexpr double CONTRAST_RATIO_EPSILON = 0.04; + +// Color spaces that measure luminance, such as Y in XYZ, L* in L*a*b*, +// or T in HCT, are known as perceptual accurate color spaces. +// +// To be displayed, they must gamut map to a "display space", one that has +// a defined limit on the number of colors. Display spaces include sRGB, +// more commonly understood as RGB/HSL/HSV/HSB. +// +// Gamut mapping is undefined and not defined by the color space. Any +// gamut mapping algorithm must choose how to sacrifice accuracy in hue, +// saturation, and/or lightness. +// +// A principled solution is to maintain lightness, thus maintaining +// contrast/a11y, maintain hue, thus maintaining aesthetic intent, and reduce +// chroma until the color is in gamut. +// +// HCT chooses this solution, but, that doesn't mean it will _exactly_ matched +// desired lightness, if only because RGB is quantized: RGB is expressed as +// a set of integers: there may be an RGB color with, for example, +// 47.892 lightness, but not 47.891. +// +// To allow for this inherent incompatibility between perceptually accurate +// color spaces and display color spaces, methods that take a contrast ratio +// and luminance, and return a luminance that reaches that contrast ratio for +// the input luminance, purposefully darken/lighten their result such that +// the desired contrast ratio will be reached even if inaccuracy is introduced. +// +// 0.4 is generous, ex. HCT requires much less delta. It was chosen because +// it provides a rough guarantee that as long as a percetual color space +// gamut maps lightness such that the resulting lightness rounds to the same +// as the requested, the desired contrast ratio will be reached. +constexpr double LUMINANCE_GAMUT_MAP_TOLERANCE = 0.4; + +double RatioOfYs(double y1, double y2) { + double lighter = y1 > y2 ? y1 : y2; + double darker = (lighter == y2) ? y1 : y2; + return (lighter + 5.0) / (darker + 5.0); +} + +double RatioOfTones(double tone_a, double tone_b) { + tone_a = std::clamp(tone_a, 0.0, 100.0); + tone_b = std::clamp(tone_b, 0.0, 100.0); + return RatioOfYs(YFromLstar(tone_a), YFromLstar(tone_b)); +} + +double Lighter(double tone, double ratio) { + if (tone < 0.0 || tone > 100.0) { + return -1.0; + } + + double dark_y = YFromLstar(tone); + double light_y = ratio * (dark_y + 5.0) - 5.0; + double real_contrast = RatioOfYs(light_y, dark_y); + double delta = abs(real_contrast - ratio); + if (real_contrast < ratio && delta > CONTRAST_RATIO_EPSILON) { + return -1; + } + + // ensure gamut mapping, which requires a 'range' on tone, will still result + // the correct ratio by darkening slightly. + double value = LstarFromY(light_y) + LUMINANCE_GAMUT_MAP_TOLERANCE; + if (value < 0 || value > 100) { + return -1; + } + return value; +} + +double Darker(double tone, double ratio) { + if (tone < 0.0 || tone > 100.0) { + return -1.0; + } + + double light_y = YFromLstar(tone); + double dark_y = ((light_y + 5.0) / ratio) - 5.0; + double real_contrast = RatioOfYs(light_y, dark_y); + + double delta = abs(real_contrast - ratio); + if (real_contrast < ratio && delta > CONTRAST_RATIO_EPSILON) { + return -1; + } + + // ensure gamut mapping, which requires a 'range' on tone, will still result + // the correct ratio by darkening slightly. + double value = LstarFromY(dark_y) - LUMINANCE_GAMUT_MAP_TOLERANCE; + if (value < 0 || value > 100) { + return -1; + } + return value; +} + +double LighterUnsafe(double tone, double ratio) { + double lighter_safe = Lighter(tone, ratio); + return (lighter_safe < 0.0) ? 100.0 : lighter_safe; +} + +double DarkerUnsafe(double tone, double ratio) { + double darker_safe = Darker(tone, ratio); + return (darker_safe < 0.0) ? 0.0 : darker_safe; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/contrast/contrast.h b/third_party/material-color/cpp/contrast/contrast.h new file mode 100644 index 0000000..6ff0bde --- /dev/null +++ b/third_party/material-color/cpp/contrast/contrast.h @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_CONTRAST_CONTRAST_H_ +#define CPP_CONTRAST_CONTRAST_H_ + +/** + * Utility methods for calculating contrast given two colors, or calculating a + * color given one color and a contrast ratio. + * + * Contrast ratio is calculated using XYZ's Y. When linearized to match human + * perception, Y becomes HCT's tone and L*a*b*'s' L*. Informally, this is the + * lightness of a color. + * + * Methods refer to tone, T in the the HCT color space. + * Tone is equivalent to L* in the L*a*b* color space, or L in the LCH color + * space. + */ +namespace material_color_utilities { +/** + * @return a contrast ratio, which ranges from 1 to 21. + * @param tone_a Tone between 0 and 100. Values outside will be clamped. + * @param tone_b Tone between 0 and 100. Values outside will be clamped. + */ +double RatioOfTones(double tone_a, double tone_b); + +/** + * @return a tone >= [tone] that ensures [ratio]. + * Return value is between 0 and 100. + * Returns -1 if [ratio] cannot be achieved with [tone]. + * + * @param tone Tone return value must contrast with. + * Range is 0 to 100. Invalid values will result in -1 being returned. + * @param ratio Contrast ratio of return value and [tone]. + * Range is 1 to 21, invalid values have undefined behavior. + */ +double Lighter(double tone, double ratio); + +/** + * @return a tone <= [tone] that ensures [ratio]. + * Return value is between 0 and 100. + * Returns -1 if [ratio] cannot be achieved with [tone]. + * + * @param tone Tone return value must contrast with. + * Range is 0 to 100. Invalid values will result in -1 being returned. + * @param ratio Contrast ratio of return value and [tone]. + * Range is 1 to 21, invalid values have undefined behavior. + */ +double Darker(double tone, double ratio); + +/** + * @return a tone >= [tone] that ensures [ratio]. + * Return value is between 0 and 100. + * Returns 100 if [ratio] cannot be achieved with [tone]. + * + * This method is unsafe because the returned value is guaranteed to be in + * bounds for tone, i.e. between 0 and 100. However, that value may not reach + * the [ratio] with [tone]. For example, there is no color lighter than T100. + * + * @param tone Tone return value must contrast with. + * Range is 0 to 100. Invalid values will result in 100 being returned. + * @param ratio Desired contrast ratio of return value and tone parameter. + * Range is 1 to 21, invalid values have undefined behavior. + */ +double LighterUnsafe(double tone, double ratio); + +/** + * @return a tone <= [tone] that ensures [ratio]. + * Return value is between 0 and 100. + * Returns 0 if [ratio] cannot be achieved with [tone]. + * + * This method is unsafe because the returned value is guaranteed to be in + * bounds for tone, i.e. between 0 and 100. However, that value may not reach + * the [ratio] with [tone]. For example, there is no color darker than T0. + * + * @param tone Tone return value must contrast with. + * Range is 0 to 100. Invalid values will result in 0 being returned. + * @param ratio Desired contrast ratio of return value and tone parameter. + * Range is 1 to 21, invalid values have undefined behavior. + */ +double DarkerUnsafe(double tone, double ratio); +} // namespace material_color_utilities + +#endif // CPP_CONTRAST_CONTRAST_H_ diff --git a/third_party/material-color/cpp/contrast/contrast_test.cc b/third_party/material-color/cpp/contrast/contrast_test.cc new file mode 100644 index 0000000..38a85c0 --- /dev/null +++ b/third_party/material-color/cpp/contrast/contrast_test.cc @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/contrast/contrast.h" + +#include "testing/base/public/gunit.h" + +namespace material_color_utilities { + +namespace { +TEST(ContrastTest, RatioOfTonesOutOfBoundsInput) { + EXPECT_NEAR(RatioOfTones(-10.0, 110.0), 21.0, 0.001); +} + +TEST(ContrastTest, LighterImpossibleRatioErrors) { + EXPECT_NEAR(Lighter(90.0, 10.0), -1.0, 0.001); +} + +TEST(ContrastTest, LighterOutOfBoundsInputAboveErrors) { + EXPECT_NEAR(Lighter(110.0, 2.0), -1.0, 0.001); +} + +TEST(ContrastTest, LighterOutOfBoundsInputBelowErrors) { + EXPECT_NEAR(Lighter(-10.0, 2.0), -1.0, 0.001); +} + +TEST(ContrastTest, LighterUnsafeReturnsMaxTone) { + EXPECT_NEAR(LighterUnsafe(100.0, 2.0), 100, 0.001); +} + +TEST(ContrastTest, DarkerImpossibleRatioErrors) { + EXPECT_NEAR(Darker(10.0, 20.0), -1.0, 0.001); +} + +TEST(ContrastTest, DarkerOutOfBoundsInputAboveErrors) { + EXPECT_NEAR(Darker(110.0, 2.0), -1.0, 0.001); +} + +TEST(ContrastTest, DarkerOutOfBoundsInputBelowErrors) { + EXPECT_NEAR(Darker(-10.0, 2.0), -1.0, 0.001); +} + +TEST(ContrastTest, DarkerUnsafeReturnsMinTone) { + EXPECT_NEAR(DarkerUnsafe(0.0, 2.0), 0.0, 0.001); +} +} // namespace + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/dislike/dislike.cc b/third_party/material-color/cpp/dislike/dislike.cc new file mode 100644 index 0000000..a07f360 --- /dev/null +++ b/third_party/material-color/cpp/dislike/dislike.cc @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/dislike/dislike.h" + +#include + +#include "cpp/cam/hct.h" + +namespace material_color_utilities { + +bool IsDisliked(Hct hct) { + double roundedHue = std::round(hct.get_hue()); + + bool hue_passes = roundedHue >= 90.0 && roundedHue <= 111.0; + bool chroma_passes = std::round(hct.get_chroma()) > 16.0; + bool tone_passes = std::round(hct.get_tone()) < 65.0; + + return hue_passes && chroma_passes && tone_passes; +} + +Hct FixIfDisliked(Hct hct) { + if (IsDisliked(hct)) { + return Hct(hct.get_hue(), hct.get_chroma(), 70.0); + } + + return hct; +} +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/dislike/dislike.h b/third_party/material-color/cpp/dislike/dislike.h new file mode 100644 index 0000000..3d09fab --- /dev/null +++ b/third_party/material-color/cpp/dislike/dislike.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_DISLIKE_DISLIKE_H_ +#define CPP_DISLIKE_DISLIKE_H_ + +#include "cpp/cam/hct.h" + +namespace material_color_utilities { + +/** + * Checks and/or fixes universally disliked colors. + * + * Color science studies of color preference indicate universal distaste for + * dark yellow-greens, and also show this is correlated to distate for + * biological waste and rotting food. + * + * See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook + * of Color Psychology (2015). + */ + +/** + * @return whether the color is disliked. + * + * Disliked is defined as a dark yellow-green that is not neutral. + * @param hct The color to be tested. + */ +bool IsDisliked(Hct hct); + +/** + * If a color is disliked, lightens it to make it likable. + * + * The original color is not modified. + * + * @param hct The color to be tested (and fixed, if needed). + * @return The original color if it is not disliked; otherwise, the fixed + * color. + */ +Hct FixIfDisliked(Hct hct); +} // namespace material_color_utilities + +#endif // CPP_DISLIKE_DISLIKE_H_ diff --git a/third_party/material-color/cpp/dislike/dislike_test.cc b/third_party/material-color/cpp/dislike/dislike_test.cc new file mode 100644 index 0000000..0d2b207 --- /dev/null +++ b/third_party/material-color/cpp/dislike/dislike_test.cc @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/dislike/dislike.h" + +#include "testing/base/public/gunit.h" +#include "cpp/cam/hct.h" + +namespace material_color_utilities { + +namespace { + +using testing::TestWithParam; +using testing::Values; +using SkinToneTest = TestWithParam; + +TEST_P(SkinToneTest, MonkSkinToneScaleColorsLiked) { + int argb = GetParam(); + + EXPECT_FALSE(IsDisliked(Hct(argb))); +} + +INSTANTIATE_TEST_SUITE_P(DislikeTest, SkinToneTest, + Values(0xfff6ede4, 0xfff3e7db, 0xfff7ead0, 0xffeadaba, + 0xffd7bd96, 0xffa07e56, 0xff825c43, 0xff604134, + 0xff3a312a, 0xff292420)); + +using BileTest = TestWithParam; + +TEST_P(BileTest, BileColorsDisliked) { + int argb = GetParam(); + + EXPECT_TRUE(IsDisliked(Hct(argb))); +} + +INSTANTIATE_TEST_SUITE_P(DislikeTest, BileTest, + Values(0xff95884B, 0xff716B40, 0xffB08E00, 0xff4C4308, + 0xff464521)); + +using BileFixingTest = TestWithParam; + +TEST_P(BileFixingTest, BileColorsFixed) { + int argb = GetParam(); + + Hct bile_color = Hct(argb); + EXPECT_TRUE(IsDisliked(bile_color)); + Hct fixed_bile_color = FixIfDisliked(bile_color); + EXPECT_FALSE(IsDisliked(fixed_bile_color)); +} + +INSTANTIATE_TEST_SUITE_P(DislikeTest, BileFixingTest, + Values(0xff95884B, 0xff716B40, 0xffB08E00, 0xff4C4308, + 0xff464521)); + + +TEST(DislikeTest, Tone67Liked) { + Hct color = Hct(100.0, 50.0, 67.0); + EXPECT_FALSE(IsDisliked(color)); + EXPECT_EQ(FixIfDisliked(color).ToInt(), color.ToInt()); +} + +} // namespace + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/dynamiccolor/contrast_curve.h b/third_party/material-color/cpp/dynamiccolor/contrast_curve.h new file mode 100644 index 0000000..4f5c2a1 --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/contrast_curve.h @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_DYNAMICCOLOR_CONTRAST_CURVE_H_ +#define CPP_DYNAMICCOLOR_CONTRAST_CURVE_H_ + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +/** + * Documents a constraint between two DynamicColors, in which their tones must + * have a certain distance from each other. + */ +struct ContrastCurve { + double low; + double normal; + double medium; + double high; + + /** + * Creates a `ContrastCurve` object. + * + * @param low Contrast requirement for contrast level -1.0 + * @param normal Contrast requirement for contrast level 0.0 + * @param medium Contrast requirement for contrast level 0.5 + * @param high Contrast requirement for contrast level 1.0 + */ + ContrastCurve(double low, double normal, double medium, double high) + : low(low), normal(normal), medium(medium), high(high) {} + + /** + * Returns the contrast ratio at a given contrast level. + * + * @param contrastLevel The contrast level. 0.0 is the default (normal); + * -1.0 is the lowest; 1.0 is the highest. + * @return The contrast ratio, a number between 1.0 and 21.0. + */ + double getContrast(double contrastLevel) { + if (contrastLevel <= -1.0) { + return low; + } else if (contrastLevel < 0.0) { + return Lerp(low, normal, (contrastLevel - (-1)) / 1); + } else if (contrastLevel < 0.5) { + return Lerp(normal, medium, (contrastLevel - 0) / 0.5); + } else if (contrastLevel < 1.0) { + return Lerp(medium, high, (contrastLevel - 0.5) / 0.5); + } else { + return high; + } + } +}; + +} // namespace material_color_utilities + +#endif // CPP_DYNAMICCOLOR_CONTRAST_CURVE_H_ diff --git a/third_party/material-color/cpp/dynamiccolor/dynamic_color.cc b/third_party/material-color/cpp/dynamiccolor/dynamic_color.cc new file mode 100644 index 0000000..bde7fc5 --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/dynamic_color.cc @@ -0,0 +1,306 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/dynamiccolor/dynamic_color.h" + +#include +#include +#include +#include +#include + +#include "cpp/cam/hct.h" +#include "cpp/contrast/contrast.h" +#include "cpp/dynamiccolor/tone_delta_pair.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +using std::function; +using std::nullopt; +using std::optional; + +using DoubleFunction = function; + +template +optional SafeCall(optional(const T&)>> f, const T& x) { + if (f == nullopt) { + return nullopt; + } else { + return f.value()(x); + } +} + +template +optional SafeCallCleanResult(optional> f, T x) { + if (f == nullopt) { + return nullopt; + } else { + return f.value()(x); + } +} + +double ForegroundTone(double bg_tone, double ratio) { + double lighter_tone = LighterUnsafe(/*tone*/ bg_tone, /*ratio*/ ratio); + double darker_tone = DarkerUnsafe(/*tone*/ bg_tone, /*ratio*/ ratio); + double lighter_ratio = RatioOfTones(lighter_tone, bg_tone); + double darker_ratio = RatioOfTones(darker_tone, bg_tone); + double prefer_lighter = TonePrefersLightForeground(bg_tone); + + if (prefer_lighter) { + double negligible_difference = + (abs(lighter_ratio - darker_ratio) < 0.1 && lighter_ratio < ratio && + darker_ratio < ratio); + return lighter_ratio >= ratio || lighter_ratio >= darker_ratio || + negligible_difference + ? lighter_tone + : darker_tone; + } else { + return darker_ratio >= ratio || darker_ratio >= lighter_ratio + ? darker_tone + : lighter_tone; + } +} + +double EnableLightForeground(double tone) { + if (TonePrefersLightForeground(tone) && !ToneAllowsLightForeground(tone)) { + return 49.0; + } + return tone; +} + +bool TonePrefersLightForeground(double tone) { return round(tone) < 60; } + +bool ToneAllowsLightForeground(double tone) { return round(tone) <= 49; } + +/** + * Default constructor. + */ +DynamicColor::DynamicColor( + std::string name, std::function palette, + std::function tone, bool is_background, + + std::optional> background, + std::optional> + second_background, + std::optional contrast_curve, + std::optional> + tone_delta_pair) + : name_(name), + palette_(palette), + tone_(tone), + is_background_(is_background), + background_(background), + second_background_(second_background), + contrast_curve_(contrast_curve), + tone_delta_pair_(tone_delta_pair) {} + +DynamicColor DynamicColor::FromPalette( + std::string name, std::function palette, + std::function tone) { + return DynamicColor(name, palette, tone, + /*is_background=*/false, + /*background=*/nullopt, + /*second_background=*/nullopt, + /*contrast_curve=*/nullopt, + /*tone_delta_pair=*/nullopt); +} + +Argb DynamicColor::GetArgb(const DynamicScheme& scheme) { + return palette_(scheme).get(GetTone(scheme)); +} + +Hct DynamicColor::GetHct(const DynamicScheme& scheme) { + return Hct(GetArgb(scheme)); +} + +double DynamicColor::GetTone(const DynamicScheme& scheme) { + bool decreasingContrast = scheme.contrast_level < 0; + + // Case 1: dual foreground, pair of colors with delta constraint. + if (tone_delta_pair_ != std::nullopt) { + ToneDeltaPair tone_delta_pair = tone_delta_pair_.value()(scheme); + DynamicColor role_a = tone_delta_pair.role_a_; + DynamicColor role_b = tone_delta_pair.role_b_; + double delta = tone_delta_pair.delta_; + TonePolarity polarity = tone_delta_pair.polarity_; + bool stay_together = tone_delta_pair.stay_together_; + + DynamicColor bg = background_.value()(scheme); + double bg_tone = bg.GetTone(scheme); + + bool a_is_nearer = + (polarity == TonePolarity::kNearer || + (polarity == TonePolarity::kLighter && !scheme.is_dark) || + (polarity == TonePolarity::kDarker && scheme.is_dark)); + DynamicColor nearer = a_is_nearer ? role_a : role_b; + DynamicColor farther = a_is_nearer ? role_b : role_a; + bool am_nearer = this->name_ == nearer.name_; + double expansion_dir = scheme.is_dark ? 1 : -1; + + // 1st round: solve to min, each + double n_contrast = + nearer.contrast_curve_.value().getContrast(scheme.contrast_level); + double f_contrast = + farther.contrast_curve_.value().getContrast(scheme.contrast_level); + + // If a color is good enough, it is not adjusted. + // Initial and adjusted tones for `nearer` + double n_initial_tone = nearer.tone_(scheme); + double n_tone = RatioOfTones(bg_tone, n_initial_tone) >= n_contrast + ? n_initial_tone + : ForegroundTone(bg_tone, n_contrast); + // Initial and adjusted tones for `farther` + double f_initial_tone = farther.tone_(scheme); + double f_tone = RatioOfTones(bg_tone, f_initial_tone) >= f_contrast + ? f_initial_tone + : ForegroundTone(bg_tone, f_contrast); + + if (decreasingContrast) { + // If decreasing contrast, adjust color to the "bare minimum" + // that satisfies contrast. + n_tone = ForegroundTone(bg_tone, n_contrast); + f_tone = ForegroundTone(bg_tone, f_contrast); + } + + if ((f_tone - n_tone) * expansion_dir >= delta) { + // Good! Tones satisfy the constraint; no change needed. + } else { + // 2nd round: expand farther to match delta. + f_tone = std::clamp(n_tone + delta * expansion_dir, 0.0, 100.0); + if ((f_tone - n_tone) * expansion_dir >= delta) { + // Good! Tones now satisfy the constraint; no change needed. + } else { + // 3rd round: contract nearer to match delta. + n_tone = std::clamp(f_tone - delta * expansion_dir, 0.0, 100.0); + } + } + + // Avoids the 50-59 awkward zone. + if (50 <= n_tone && n_tone < 60) { + // If `nearer` is in the awkward zone, move it away, together with + // `farther`. + if (expansion_dir > 0) { + n_tone = 60; + f_tone = std::max(f_tone, n_tone + delta * expansion_dir); + } else { + n_tone = 49; + f_tone = std::min(f_tone, n_tone + delta * expansion_dir); + } + } else if (50 <= f_tone && f_tone < 60) { + if (stay_together) { + // Fixes both, to avoid two colors on opposite sides of the "awkward + // zone". + if (expansion_dir > 0) { + n_tone = 60; + f_tone = std::max(f_tone, n_tone + delta * expansion_dir); + } else { + n_tone = 49; + f_tone = std::min(f_tone, n_tone + delta * expansion_dir); + } + } else { + // Not required to stay together; fixes just one. + if (expansion_dir > 0) { + f_tone = 60; + } else { + f_tone = 49; + } + } + } + + // Returns `n_tone` if this color is `nearer`, otherwise `f_tone`. + return am_nearer ? n_tone : f_tone; + } else { + // Case 2: No contrast pair; just solve for itself. + double answer = tone_(scheme); + + if (background_ == std::nullopt) { + return answer; // No adjustment for colors with no background. + } + + double bg_tone = background_.value()(scheme).GetTone(scheme); + + double desired_ratio = + contrast_curve_.value().getContrast(scheme.contrast_level); + + if (RatioOfTones(bg_tone, answer) >= desired_ratio) { + // Don't "improve" what's good enough. + } else { + // Rough improvement. + answer = ForegroundTone(bg_tone, desired_ratio); + } + + if (decreasingContrast) { + answer = ForegroundTone(bg_tone, desired_ratio); + } + + if (is_background_ && 50 <= answer && answer < 60) { + // Must adjust + if (RatioOfTones(49, bg_tone) >= desired_ratio) { + answer = 49; + } else { + answer = 60; + } + } + + if (second_background_ != std::nullopt) { + // Case 3: Adjust for dual backgrounds. + + double bg_tone_1 = background_.value()(scheme).GetTone(scheme); + double bg_tone_2 = second_background_.value()(scheme).GetTone(scheme); + + double upper = std::max(bg_tone_1, bg_tone_2); + double lower = std::min(bg_tone_1, bg_tone_2); + + if (RatioOfTones(upper, answer) >= desired_ratio && + RatioOfTones(lower, answer) >= desired_ratio) { + return answer; + } + + // The darkest light tone that satisfies the desired ratio, + // or -1 if such ratio cannot be reached. + double lightOption = Lighter(upper, desired_ratio); + + // The lightest dark tone that satisfies the desired ratio, + // or -1 if such ratio cannot be reached. + double darkOption = Darker(lower, desired_ratio); + + // Tones suitable for the foreground. + std::vector availables; + if (lightOption != -1) { + availables.push_back(lightOption); + } + if (darkOption != -1) { + availables.push_back(darkOption); + } + + bool prefersLight = TonePrefersLightForeground(bg_tone_1) || + TonePrefersLightForeground(bg_tone_2); + if (prefersLight) { + return (lightOption < 0) ? 100 : lightOption; + } + if (availables.size() == 1) { + return availables[0]; + } + return (darkOption < 0) ? 0 : darkOption; + } + + return answer; + } +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/dynamiccolor/dynamic_color.h b/third_party/material-color/cpp/dynamiccolor/dynamic_color.h new file mode 100644 index 0000000..3678afc --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/dynamic_color.h @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_DYNAMICCOLOR_DYNAMIC_COLOR_H_ +#define CPP_DYNAMICCOLOR_DYNAMIC_COLOR_H_ + +#include +#include +#include + +#include "cpp/cam/hct.h" +#include "cpp/dynamiccolor/contrast_curve.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +struct ToneDeltaPair; + +/** + * Given a background tone, find a foreground tone, while ensuring they reach + * a contrast ratio that is as close to [ratio] as possible. + * + * [bgTone] Tone in HCT. Range is 0 to 100, undefined behavior when it falls + * outside that range. + * [ratio] The contrast ratio desired between [bgTone] and the return value. + */ +double ForegroundTone(double bg_tone, double ratio); + +/** + * Adjust a tone such that white has 4.5 contrast, if the tone is + * reasonably close to supporting it. + */ +double EnableLightForeground(double tone); + +/** + * Returns whether [tone] prefers a light foreground. + * + * People prefer white foregrounds on ~T60-70. Observed over time, and also + * by Andrew Somers during research for APCA. + * + * T60 used as to create the smallest discontinuity possible when skipping + * down to T49 in order to ensure light foregrounds. + * + * Since `tertiaryContainer` in dark monochrome scheme requires a tone of + * 60, it should not be adjusted. Therefore, 60 is excluded here. + */ +bool TonePrefersLightForeground(double tone); + +/** + * Returns whether [tone] can reach a contrast ratio of 4.5 with a lighter + * color. + */ +bool ToneAllowsLightForeground(double tone); + +/** + * @param name_ The name of the dynamic color. + * @param palette_ Function that provides a TonalPalette given + * DynamicScheme. A TonalPalette is defined by a hue and chroma, so this + * replaces the need to specify hue/chroma. By providing a tonal palette, when + * contrast adjustments are made, intended chroma can be preserved. + * @param tone_ Function that provides a tone given DynamicScheme. + * @param is_background_ Whether this dynamic color is a background, with + * some other color as the foreground. + * @param background_ The background of the dynamic color (as a function of a + * `DynamicScheme`), if it exists. + * @param second_background_ A second background of the dynamic color (as a + * function of a `DynamicScheme`), if it + * exists. + * @param contrast_curve_ A `ContrastCurve` object specifying how its contrast + * against its background should behave in various contrast levels options. + * @param tone_delta_pair_ A `ToneDeltaPair` object specifying a tone delta + * constraint between two colors. One of them must be the color being + * constructed. + */ +struct DynamicColor { + std::string name_; + std::function palette_; + std::function tone_; + bool is_background_; + + std::optional> background_; + std::optional> + second_background_; + std::optional contrast_curve_; + std::optional> + tone_delta_pair_; + + /** A convenience constructor, only requiring name, palette, and tone. */ + static DynamicColor FromPalette( + std::string name, + std::function palette, + std::function tone); + + Argb GetArgb(const DynamicScheme& scheme); + + Hct GetHct(const DynamicScheme& scheme); + + double GetTone(const DynamicScheme& scheme); + + /** The default constructor. */ + DynamicColor(std::string name, + std::function palette, + std::function tone, + bool is_background, + + std::optional> + background, + std::optional> + second_background, + std::optional contrast_curve, + std::optional> + tone_delta_pair); +}; + +} // namespace material_color_utilities + +#endif // CPP_DYNAMICCOLOR_DYNAMIC_COLOR_H_ diff --git a/third_party/material-color/cpp/dynamiccolor/dynamic_color_test.cc b/third_party/material-color/cpp/dynamiccolor/dynamic_color_test.cc new file mode 100644 index 0000000..78111b1 --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/dynamic_color_test.cc @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/dynamiccolor/dynamic_color.h" + +#include "testing/base/public/gunit.h" +#include "cpp/cam/hct.h" +#include "cpp/dynamiccolor/material_dynamic_colors.h" +#include "cpp/scheme/scheme_vibrant.h" + +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace material_color_utilities { + +namespace { +TEST(DynamicColorTest, One) { + const SchemeVibrant s = SchemeVibrant(Hct(0xFFFF0000), false, 0.5); + + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Background().GetArgb(s)), + 0xfffff8f6); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OnBackground().GetArgb(s)), + 0xff271815); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Surface().GetArgb(s)), + 0xfffff8f6); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::SurfaceDim().GetArgb(s)), + 0xfff0d4cf); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::SurfaceBright().GetArgb(s)), + 0xfffff8f6); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::SurfaceContainerLowest().GetArgb( + s)), + 0xffffffff); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::SurfaceContainerLow().GetArgb(s)), + 0xfffff0ee); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::SurfaceContainer().GetArgb(s)), + 0xffffe9e6); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::SurfaceContainerHigh().GetArgb(s)), + 0xffffe2dd); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::SurfaceContainerHighest().GetArgb( + s)), + 0xfff9dcd8); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OnSurface().GetArgb(s)), + 0xff271815); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::SurfaceVariant().GetArgb(s)), + 0xfffddbd5); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::OnSurfaceVariant().GetArgb(s)), + 0xff543d3a); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::InverseSurface().GetArgb(s)), + 0xff3d2c29); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::InverseOnSurface().GetArgb(s)), + 0xffffedea); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Outline().GetArgb(s)), + 0xff725955); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OutlineVariant().GetArgb(s)), + 0xff907470); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Shadow().GetArgb(s)), + 0xff000000); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Scrim().GetArgb(s)), + 0xff000000); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::SurfaceTint().GetArgb(s)), + 0xffc00100); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Primary().GetArgb(s)), + 0xff8c0100); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OnPrimary().GetArgb(s)), + 0xffffffff); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::PrimaryContainer().GetArgb(s)), + 0xffeb0000); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::OnPrimaryContainer().GetArgb(s)), + 0xffffffff); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::InversePrimary().GetArgb(s)), + 0xffffb4a8); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Secondary().GetArgb(s)), + 0xff603924); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OnSecondary().GetArgb(s)), + 0xffffffff); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::SecondaryContainer().GetArgb(s)), + 0xff996952); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::OnSecondaryContainer().GetArgb(s)), + 0xffffffff); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Tertiary().GetArgb(s)), + 0xff633909); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OnTertiary().GetArgb(s)), + 0xffffffff); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::TertiaryContainer().GetArgb(s)), + 0xff9d6937); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::OnTertiaryContainer().GetArgb(s)), + 0xffffffff); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::Error().GetArgb(s)), + 0xff8c0009); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::OnError().GetArgb(s)), + 0xffffffff); + EXPECT_EQ((unsigned int)(MaterialDynamicColors::ErrorContainer().GetArgb(s)), + 0xffda342e); + EXPECT_EQ( + (unsigned int)(MaterialDynamicColors::OnErrorContainer().GetArgb(s)), + 0xffffffff); +} + +} // namespace + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/dynamiccolor/material_dynamic_colors.cc b/third_party/material-color/cpp/dynamiccolor/material_dynamic_colors.cc new file mode 100644 index 0000000..c722aef --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/material_dynamic_colors.cc @@ -0,0 +1,1109 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/dynamiccolor/material_dynamic_colors.h" + +#include + +#include "cpp/cam/cam.h" +#include "cpp/cam/hct.h" +#include "cpp/dislike/dislike.h" +#include "cpp/dynamiccolor/contrast_curve.h" +#include "cpp/dynamiccolor/dynamic_color.h" +#include "cpp/dynamiccolor/tone_delta_pair.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +using std::nullopt; + +bool IsFidelity(const DynamicScheme& scheme) { + return scheme.variant == Variant::kFidelity || + scheme.variant == Variant::kContent; +} + +bool IsMonochrome(const DynamicScheme& scheme) { + return scheme.variant == Variant::kMonochrome; +} + +Vec3 XyzInViewingConditions(Cam cam, ViewingConditions viewing_conditions) { + double alpha = (cam.chroma == 0.0 || cam.j == 0.0) + ? 0.0 + : cam.chroma / sqrt(cam.j / 100.0); + + double t = pow( + alpha / pow(1.64 - pow(0.29, + viewing_conditions.background_y_to_white_point_y), + 0.73), + 1.0 / 0.9); + double h_rad = cam.hue * M_PI / 180.0; + + double e_hue = 0.25 * (cos(h_rad + 2.0) + 3.8); + double ac = + viewing_conditions.aw * + pow(cam.j / 100.0, 1.0 / viewing_conditions.c / viewing_conditions.z); + double p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.n_c * + viewing_conditions.ncb; + + double p2 = (ac / viewing_conditions.nbb); + + double h_sin = sin(h_rad); + double h_cos = cos(h_rad); + + double gamma = 23.0 * (p2 + 0.305) * t / + (23.0 * p1 + 11 * t * h_cos + 108.0 * t * h_sin); + double a = gamma * h_cos; + double b = gamma * h_sin; + double r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0; + double g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0; + double b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0; + + double r_c_base = fmax(0, (27.13 * fabs(r_a)) / (400.0 - fabs(r_a))); + double r_c = + Signum(r_a) * (100.0 / viewing_conditions.fl) * pow(r_c_base, 1.0 / 0.42); + double g_c_base = fmax(0, (27.13 * fabs(g_a)) / (400.0 - fabs(g_a))); + double g_c = + Signum(g_a) * (100.0 / viewing_conditions.fl) * pow(g_c_base, 1.0 / 0.42); + double b_c_base = fmax(0, (27.13 * fabs(b_a)) / (400.0 - fabs(b_a))); + double b_c = + Signum(b_a) * (100.0 / viewing_conditions.fl) * pow(b_c_base, 1.0 / 0.42); + double r_f = r_c / viewing_conditions.rgb_d[0]; + double g_f = g_c / viewing_conditions.rgb_d[1]; + double b_f = b_c / viewing_conditions.rgb_d[2]; + + double x = 1.86206786 * r_f - 1.01125463 * g_f + 0.14918677 * b_f; + double y = 0.38752654 * r_f + 0.62144744 * g_f - 0.00897398 * b_f; + double z = -0.01584150 * r_f - 0.03412294 * g_f + 1.04996444 * b_f; + + return {x, y, z}; +} + +Hct InViewingConditions(Hct hct, ViewingConditions vc) { + // 1. Use CAM16 to find XYZ coordinates of color in specified VC. + Cam cam16 = CamFromInt(hct.ToInt()); + Vec3 viewed_in_vc = XyzInViewingConditions(cam16, vc); + + // 2. Create CAM16 of those XYZ coordinates in default VC. + Cam recast_in_vc = + CamFromXyzAndViewingConditions(viewed_in_vc.a, viewed_in_vc.b, + viewed_in_vc.c, kDefaultViewingConditions); + + // 3. Create HCT from: + // - CAM16 using default VC with XYZ coordinates in specified VC. + // - L* converted from Y in XYZ coordinates in specified VC. + Hct recast_hct = + Hct(recast_in_vc.hue, recast_in_vc.chroma, LstarFromY(viewed_in_vc.b)); + return recast_hct; +} + +double FindDesiredChromaByTone(double hue, double chroma, double tone, + bool by_decreasing_tone) { + double answer = tone; + + Hct closest_to_chroma = Hct(hue, chroma, tone); + if (closest_to_chroma.get_chroma() < chroma) { + double chroma_peak = closest_to_chroma.get_chroma(); + while (closest_to_chroma.get_chroma() < chroma) { + answer += by_decreasing_tone ? -1.0 : 1.0; + Hct potential_solution = Hct(hue, chroma, answer); + if (chroma_peak > potential_solution.get_chroma()) { + break; + } + if (abs(potential_solution.get_chroma() - chroma) < 0.4) { + break; + } + + double potential_delta = abs(potential_solution.get_chroma() - chroma); + double current_delta = abs(closest_to_chroma.get_chroma() - chroma); + if (potential_delta < current_delta) { + closest_to_chroma = potential_solution; + } + chroma_peak = fmax(chroma_peak, potential_solution.get_chroma()); + } + } + + return answer; +} + +constexpr double kContentAccentToneDelta = 15.0; +DynamicColor highestSurface(const DynamicScheme& s) { + return s.is_dark ? MaterialDynamicColors::SurfaceBright() + : MaterialDynamicColors::SurfaceDim(); +} + +// Compatibility Keys Colors for Android +DynamicColor MaterialDynamicColors::PrimaryPaletteKeyColor() { + return DynamicColor::FromPalette( + "primary_palette_key_color", + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + [](const DynamicScheme& s) -> double { + return s.primary_palette.get_key_color().get_tone(); + }); +} + +DynamicColor MaterialDynamicColors::SecondaryPaletteKeyColor() { + return DynamicColor::FromPalette( + "secondary_palette_key_color", + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + [](const DynamicScheme& s) -> double { + return s.secondary_palette.get_key_color().get_tone(); + }); +} + +DynamicColor MaterialDynamicColors::TertiaryPaletteKeyColor() { + return DynamicColor::FromPalette( + "tertiary_palette_key_color", + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + [](const DynamicScheme& s) -> double { + return s.tertiary_palette.get_key_color().get_tone(); + }); +} + +DynamicColor MaterialDynamicColors::NeutralPaletteKeyColor() { + return DynamicColor::FromPalette( + "neutral_palette_key_color", + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + [](const DynamicScheme& s) -> double { + return s.neutral_palette.get_key_color().get_tone(); + }); +} + +DynamicColor MaterialDynamicColors::NeutralVariantPaletteKeyColor() { + return DynamicColor::FromPalette( + "neutral_variant_palette_key_color", + [](const DynamicScheme& s) -> TonalPalette { + return s.neutral_variant_palette; + }, + [](const DynamicScheme& s) -> double { + return s.neutral_variant_palette.get_key_color().get_tone(); + }); +} + +DynamicColor MaterialDynamicColors::Background() { + return DynamicColor( + /* name= */ "background", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 6.0 : 98.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OnBackground() { + return DynamicColor( + /* name= */ "on_background", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 90.0 : 10.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return Background(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 3.0, 4.5, 7.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Surface() { + return DynamicColor( + /* name= */ "surface", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 6.0 : 98.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceDim() { + return DynamicColor( + /* name= */ "surface_dim", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 6.0 : 87.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceBright() { + return DynamicColor( + /* name= */ "surface_bright", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 24.0 : 98.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceContainerLowest() { + return DynamicColor( + /* name= */ "surface_container_lowest", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 4.0 : 100.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceContainerLow() { + return DynamicColor( + /* name= */ "surface_container_low", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 10.0 : 96.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceContainer() { + return DynamicColor( + /* name= */ "surface_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 12.0 : 94.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceContainerHigh() { + return DynamicColor( + /* name= */ "surface_container_high", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 17.0 : 92.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceContainerHighest() { + return DynamicColor( + /* name= */ "surface_container_highest", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 22.0 : 90.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OnSurface() { + return DynamicColor( + /* name= */ "on_surface", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 90.0 : 10.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceVariant() { + return DynamicColor( + /* name= */ "surface_variant", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.neutral_variant_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 30.0 : 90.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OnSurfaceVariant() { + return DynamicColor( + /* name= */ "on_surface_variant", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.neutral_variant_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 80.0 : 30.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::InverseSurface() { + return DynamicColor( + /* name= */ "inverse_surface", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 90.0 : 20.0; }, + /* isBackground= */ false, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::InverseOnSurface() { + return DynamicColor( + /* name= */ "inverse_on_surface", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 20.0 : 95.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return InverseSurface(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Outline() { + return DynamicColor( + /* name= */ "outline", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.neutral_variant_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 60.0 : 50.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.5, 3.0, 4.5, 7.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OutlineVariant() { + return DynamicColor( + /* name= */ "outline_variant", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.neutral_variant_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 30.0 : 80.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Shadow() { + return DynamicColor( + /* name= */ "shadow", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ [](const DynamicScheme& s) -> double { return 0.0; }, + /* isBackground= */ false, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Scrim() { + return DynamicColor( + /* name= */ "scrim", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.neutral_palette; }, + /* tone= */ [](const DynamicScheme& s) -> double { return 0.0; }, + /* isBackground= */ false, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SurfaceTint() { + return DynamicColor( + /* name= */ "surface_tint", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 80.0 : 40.0; }, + /* isBackground= */ true, + /* background= */ nullopt, + /* secondBackground= */ nullopt, + /* contrastCurve= */ nullopt, + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Primary() { + return DynamicColor( + /* name= */ "primary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 100.0 : 0.0; + } + return s.is_dark ? 80.0 : 40.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(PrimaryContainer(), Primary(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnPrimary() { + return DynamicColor( + /* name= */ "on_primary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 10.0 : 90.0; + } + return s.is_dark ? 20.0 : 100.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return Primary(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::PrimaryContainer() { + return DynamicColor( + /* name= */ "primary_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsFidelity(s)) { + return s.source_color_hct.get_tone(); + } + if (IsMonochrome(s)) { + return s.is_dark ? 85.0 : 25.0; + } + return s.is_dark ? 30.0 : 90.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(PrimaryContainer(), Primary(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnPrimaryContainer() { + return DynamicColor( + /* name= */ "on_primary_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsFidelity(s)) { + return ForegroundTone(PrimaryContainer().tone_(s), 4.5); + } + if (IsMonochrome(s)) { + return s.is_dark ? 0.0 : 100.0; + } + return s.is_dark ? 90.0 : 10.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return PrimaryContainer(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::InversePrimary() { + return DynamicColor( + /* name= */ "inverse_primary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 40.0 : 80.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return InverseSurface(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Secondary() { + return DynamicColor( + /* name= */ "secondary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 80.0 : 40.0; }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(SecondaryContainer(), Secondary(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnSecondary() { + return DynamicColor( + /* name= */ "on_secondary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 10.0 : 100.0; + } else { + return s.is_dark ? 20.0 : 100.0; + } + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return Secondary(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SecondaryContainer() { + return DynamicColor( + /* name= */ "secondary_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { + double initialTone = s.is_dark ? 30.0 : 90.0; + if (IsMonochrome(s)) { + return s.is_dark ? 30.0 : 85.0; + } + if (!IsFidelity(s)) { + return initialTone; + } + return FindDesiredChromaByTone(s.secondary_palette.get_hue(), + s.secondary_palette.get_chroma(), + initialTone, s.is_dark ? false : true); + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(SecondaryContainer(), Secondary(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnSecondaryContainer() { + return DynamicColor( + /* name= */ "on_secondary_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (!IsFidelity(s)) { + return s.is_dark ? 90.0 : 10.0; + } + return ForegroundTone(SecondaryContainer().tone_(s), 4.5); + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { + return SecondaryContainer(); + }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Tertiary() { + return DynamicColor( + /* name= */ "tertiary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 90.0 : 25.0; + } + return s.is_dark ? 80.0 : 40.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(TertiaryContainer(), Tertiary(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnTertiary() { + return DynamicColor( + /* name= */ "on_tertiary", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 10.0 : 90.0; + } + return s.is_dark ? 20.0 : 100.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return Tertiary(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::TertiaryContainer() { + return DynamicColor( + /* name= */ "tertiary_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 60.0 : 49.0; + } + if (!IsFidelity(s)) { + return s.is_dark ? 30.0 : 90.0; + } + Hct proposedHct = + Hct(s.tertiary_palette.get(s.source_color_hct.get_tone())); + return FixIfDisliked(proposedHct).get_tone(); + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(TertiaryContainer(), Tertiary(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnTertiaryContainer() { + return DynamicColor( + /* name= */ "on_tertiary_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + if (IsMonochrome(s)) { + return s.is_dark ? 0.0 : 100.0; + } + if (!IsFidelity(s)) { + return s.is_dark ? 90.0 : 10.0; + } + return ForegroundTone(TertiaryContainer().tone_(s), 4.5); + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { + return TertiaryContainer(); + }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::Error() { + return DynamicColor( + /* name= */ "error", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.error_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 80.0 : 40.0; }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(ErrorContainer(), Error(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnError() { + return DynamicColor( + /* name= */ "on_error", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.error_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 20.0 : 100.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return Error(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::ErrorContainer() { + return DynamicColor( + /* name= */ "error_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.error_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 30.0 : 90.0; }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(ErrorContainer(), Error(), 15.0, + TonePolarity::kNearer, false); + }); +} + +DynamicColor MaterialDynamicColors::OnErrorContainer() { + return DynamicColor( + /* name= */ "on_error_container", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.error_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { return s.is_dark ? 90.0 : 10.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return ErrorContainer(); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::PrimaryFixed() { + return DynamicColor( + /* name= */ "primary_fixed", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 40.0 : 90.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(PrimaryFixed(), PrimaryFixedDim(), 10.0, + TonePolarity::kLighter, true); + }); +} + +DynamicColor MaterialDynamicColors::PrimaryFixedDim() { + return DynamicColor( + /* name= */ "primary_fixed_dim", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 30.0 : 80.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(PrimaryFixed(), PrimaryFixedDim(), 10.0, + TonePolarity::kLighter, true); + }); +} + +DynamicColor MaterialDynamicColors::OnPrimaryFixed() { + return DynamicColor( + /* name= */ "on_primary_fixed", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 100.0 : 10.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return PrimaryFixedDim(); }, + /* secondBackground= */ + [](const DynamicScheme& s) -> DynamicColor { return PrimaryFixed(); }, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OnPrimaryFixedVariant() { + return DynamicColor( + /* name= */ "on_primary_fixed_variant", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.primary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 90.0 : 30.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return PrimaryFixedDim(); }, + /* secondBackground= */ + [](const DynamicScheme& s) -> DynamicColor { return PrimaryFixed(); }, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::SecondaryFixed() { + return DynamicColor( + /* name= */ "secondary_fixed", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 80.0 : 90.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(SecondaryFixed(), SecondaryFixedDim(), 10.0, + TonePolarity::kLighter, true); + }); +} + +DynamicColor MaterialDynamicColors::SecondaryFixedDim() { + return DynamicColor( + /* name= */ "secondary_fixed_dim", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 70.0 : 80.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(SecondaryFixed(), SecondaryFixedDim(), 10.0, + TonePolarity::kLighter, true); + }); +} + +DynamicColor MaterialDynamicColors::OnSecondaryFixed() { + return DynamicColor( + /* name= */ "on_secondary_fixed", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ [](const DynamicScheme& s) -> double { return 10.0; }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { + return SecondaryFixedDim(); + }, + /* secondBackground= */ + [](const DynamicScheme& s) -> DynamicColor { return SecondaryFixed(); }, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OnSecondaryFixedVariant() { + return DynamicColor( + /* name= */ "on_secondary_fixed_variant", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { + return s.secondary_palette; + }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 25.0 : 30.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { + return SecondaryFixedDim(); + }, + /* secondBackground= */ + [](const DynamicScheme& s) -> DynamicColor { return SecondaryFixed(); }, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::TertiaryFixed() { + return DynamicColor( + /* name= */ "tertiary_fixed", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 40.0 : 90.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(TertiaryFixed(), TertiaryFixedDim(), 10.0, + TonePolarity::kLighter, true); + }); +} + +DynamicColor MaterialDynamicColors::TertiaryFixedDim() { + return DynamicColor( + /* name= */ "tertiary_fixed_dim", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 30.0 : 80.0; + }, + /* isBackground= */ true, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return highestSurface(s); }, + /* secondBackground= */ nullopt, + /* contrastCurve= */ ContrastCurve(1.0, 1.0, 3.0, 7.0), + /* toneDeltaPair= */ + [](const DynamicScheme& s) -> ToneDeltaPair { + return ToneDeltaPair(TertiaryFixed(), TertiaryFixedDim(), 10.0, + TonePolarity::kLighter, true); + }); +} + +DynamicColor MaterialDynamicColors::OnTertiaryFixed() { + return DynamicColor( + /* name= */ "on_tertiary_fixed", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 100.0 : 10.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return TertiaryFixedDim(); }, + /* secondBackground= */ + [](const DynamicScheme& s) -> DynamicColor { return TertiaryFixed(); }, + /* contrastCurve= */ ContrastCurve(4.5, 7.0, 11.0, 21.0), + /* toneDeltaPair= */ nullopt); +} + +DynamicColor MaterialDynamicColors::OnTertiaryFixedVariant() { + return DynamicColor( + /* name= */ "on_tertiary_fixed_variant", + /* palette= */ + [](const DynamicScheme& s) -> TonalPalette { return s.tertiary_palette; }, + /* tone= */ + [](const DynamicScheme& s) -> double { + return IsMonochrome(s) ? 90.0 : 30.0; + }, + /* isBackground= */ false, + /* background= */ + [](const DynamicScheme& s) -> DynamicColor { return TertiaryFixedDim(); }, + /* secondBackground= */ + [](const DynamicScheme& s) -> DynamicColor { return TertiaryFixed(); }, + /* contrastCurve= */ ContrastCurve(3.0, 4.5, 7.0, 11.0), + /* toneDeltaPair= */ nullopt); +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/dynamiccolor/material_dynamic_colors.h b/third_party/material-color/cpp/dynamiccolor/material_dynamic_colors.h new file mode 100644 index 0000000..35a0af5 --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/material_dynamic_colors.h @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_DYNAMICCOLOR_MATERIAL_DYNAMIC_COLORS_H_ +#define CPP_DYNAMICCOLOR_MATERIAL_DYNAMIC_COLORS_H_ + +#include "cpp/dynamiccolor/dynamic_color.h" + +namespace material_color_utilities { + +class MaterialDynamicColors { + public: + static DynamicColor PrimaryPaletteKeyColor(); + static DynamicColor SecondaryPaletteKeyColor(); + static DynamicColor TertiaryPaletteKeyColor(); + static DynamicColor NeutralPaletteKeyColor(); + static DynamicColor NeutralVariantPaletteKeyColor(); + static DynamicColor Background(); + static DynamicColor OnBackground(); + static DynamicColor Surface(); + static DynamicColor SurfaceDim(); + static DynamicColor SurfaceBright(); + static DynamicColor SurfaceContainerLowest(); + static DynamicColor SurfaceContainerLow(); + static DynamicColor SurfaceContainer(); + static DynamicColor SurfaceContainerHigh(); + static DynamicColor SurfaceContainerHighest(); + static DynamicColor OnSurface(); + static DynamicColor SurfaceVariant(); + static DynamicColor OnSurfaceVariant(); + static DynamicColor InverseSurface(); + static DynamicColor InverseOnSurface(); + static DynamicColor Outline(); + static DynamicColor OutlineVariant(); + static DynamicColor Shadow(); + static DynamicColor Scrim(); + static DynamicColor SurfaceTint(); + static DynamicColor Primary(); + static DynamicColor OnPrimary(); + static DynamicColor PrimaryContainer(); + static DynamicColor OnPrimaryContainer(); + static DynamicColor InversePrimary(); + static DynamicColor Secondary(); + static DynamicColor OnSecondary(); + static DynamicColor SecondaryContainer(); + static DynamicColor OnSecondaryContainer(); + static DynamicColor Tertiary(); + static DynamicColor OnTertiary(); + static DynamicColor TertiaryContainer(); + static DynamicColor OnTertiaryContainer(); + static DynamicColor Error(); + static DynamicColor OnError(); + static DynamicColor ErrorContainer(); + static DynamicColor OnErrorContainer(); + static DynamicColor PrimaryFixed(); + static DynamicColor PrimaryFixedDim(); + static DynamicColor OnPrimaryFixed(); + static DynamicColor OnPrimaryFixedVariant(); + static DynamicColor SecondaryFixed(); + static DynamicColor SecondaryFixedDim(); + static DynamicColor OnSecondaryFixed(); + static DynamicColor OnSecondaryFixedVariant(); + static DynamicColor TertiaryFixed(); + static DynamicColor TertiaryFixedDim(); + static DynamicColor OnTertiaryFixed(); + static DynamicColor OnTertiaryFixedVariant(); +}; + +} // namespace material_color_utilities + +#endif // CPP_DYNAMICCOLOR_MATERIAL_DYNAMIC_COLORS_H_ diff --git a/third_party/material-color/cpp/dynamiccolor/tone_delta_pair.h b/third_party/material-color/cpp/dynamiccolor/tone_delta_pair.h new file mode 100644 index 0000000..1e44950 --- /dev/null +++ b/third_party/material-color/cpp/dynamiccolor/tone_delta_pair.h @@ -0,0 +1,78 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_DYNAMICCOLOR_TONE_DELTA_PAIR_H_ +#define CPP_DYNAMICCOLOR_TONE_DELTA_PAIR_H_ + +#include "cpp/dynamiccolor/dynamic_color.h" + +namespace material_color_utilities { + +/** + * Describes the different in tone between colors. + */ +enum class TonePolarity { kDarker, kLighter, kNearer, kFarther }; + +/** + * Documents a constraint between two DynamicColors, in which their tones must + * have a certain distance from each other. + * + * Prefer a DynamicColor with a background, this is for special cases when + * designers want tonal distance, literally contrast, between two colors that + * don't have a background / foreground relationship or a contrast guarantee. + */ +struct ToneDeltaPair { + DynamicColor role_a_; + DynamicColor role_b_; + double delta_; + TonePolarity polarity_; + bool stay_together_; + + /** + * Documents a constraint in tone distance between two DynamicColors. + * + * The polarity is an adjective that describes "A", compared to "B". + * + * For instance, ToneDeltaPair(A, B, 15, 'darker', stayTogether) states that + * A's tone should be at least 15 darker than B's. + * + * 'nearer' and 'farther' describes closeness to the surface roles. For + * instance, ToneDeltaPair(A, B, 10, 'nearer', stayTogether) states that A + * should be 10 lighter than B in light mode, and 10 darker than B in dark + * mode. + * + * @param roleA The first role in a pair. + * @param roleB The second role in a pair. + * @param delta Required difference between tones. Absolute value, negative + * values have undefined behavior. + * @param polarity The relative relation between tones of roleA and roleB, + * as described above. + * @param stayTogether Whether these two roles should stay on the same side of + * the "awkward zone" (T50-59). This is necessary for certain cases where + * one role has two backgrounds. + */ + ToneDeltaPair(DynamicColor role_a, DynamicColor role_b, double delta, + TonePolarity polarity, bool stay_together) + : role_a_(role_a), + role_b_(role_b), + delta_(delta), + polarity_(polarity), + stay_together_(stay_together) {} +}; + +} // namespace material_color_utilities + +#endif // CPP_DYNAMICCOLOR_TONE_DELTA_PAIR_H_ diff --git a/third_party/material-color/cpp/palettes/core.cc b/third_party/material-color/cpp/palettes/core.cc new file mode 100644 index 0000000..1043960 --- /dev/null +++ b/third_party/material-color/cpp/palettes/core.cc @@ -0,0 +1,88 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/palettes/core.h" + +#include + +#include "cpp/cam/cam.h" +#include "cpp/palettes/tones.h" + +namespace material_color_utilities { + +namespace { + +double PrimaryChroma(double chroma, bool is_content) { + return is_content ? chroma : fmax(chroma, 48); +} + +double SecondaryChroma(double chroma, bool is_content) { + return is_content ? chroma / 3 : 16; +} + +double TertiaryChroma(double chroma, bool is_content) { + return is_content ? chroma / 2 : 24; +} + +double NeutralChroma(double chroma, bool is_content) { + return is_content ? fmin(chroma / 12, 4) : 4; +} + +double NeutralVariantChroma(double chroma, bool is_content) { + return is_content ? fmin(chroma / 6, 8) : 8; +} + +} // namespace + +CorePalette::CorePalette(double hue, double chroma, bool is_content) + : primary_(hue, PrimaryChroma(chroma, is_content)), + secondary_(hue, SecondaryChroma(chroma, is_content)), + tertiary_(hue + 60, TertiaryChroma(chroma, is_content)), + neutral_(hue, NeutralChroma(chroma, is_content)), + neutral_variant_(hue, NeutralVariantChroma(chroma, is_content)), + error_(25, 84) {} + +CorePalette CorePalette::Of(double hue, double chroma) { + return CorePalette(hue, chroma, false); +} + +CorePalette CorePalette::ContentOf(double hue, double chroma) { + return CorePalette(hue, chroma, true); +} + +CorePalette CorePalette::Of(int argb) { + Cam cam = CamFromInt(argb); + return CorePalette(cam.hue, cam.chroma, false); +} + +CorePalette CorePalette::ContentOf(int argb) { + Cam cam = CamFromInt(argb); + return CorePalette(cam.hue, cam.chroma, true); +} + +TonalPalette CorePalette::primary() { return primary_; } + +TonalPalette CorePalette::secondary() { return secondary_; } + +TonalPalette CorePalette::tertiary() { return tertiary_; } + +TonalPalette CorePalette::neutral() { return neutral_; } + +TonalPalette CorePalette::neutral_variant() { return neutral_variant_; } + +TonalPalette CorePalette::error() { return error_; } + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/palettes/core.h b/third_party/material-color/cpp/palettes/core.h new file mode 100644 index 0000000..92c000a --- /dev/null +++ b/third_party/material-color/cpp/palettes/core.h @@ -0,0 +1,71 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_PALETTES_CORE_H_ +#define CPP_PALETTES_CORE_H_ + +#include "cpp/palettes/tones.h" + +namespace material_color_utilities { + +/** + * An intermediate concept between the key color for a UI theme, and a full + * color scheme. 5 tonal palettes are generated, all except one use the same + * hue as the key color, and all vary in chroma. + */ +class CorePalette { + public: + /** + * Creates a CorePalette from a hue and a chroma. + */ + static CorePalette Of(double hue, double chroma); + + /** + * Creates a CorePalette from a source color in ARGB format. + */ + static CorePalette Of(int argb); + + /** + * Creates a content CorePalette from a hue and a chroma. + */ + static CorePalette ContentOf(double hue, double chroma); + + /** + * Creates a content CorePalette from a source color in ARGB format. + */ + static CorePalette ContentOf(int argb); + + TonalPalette primary(); + TonalPalette secondary(); + TonalPalette tertiary(); + TonalPalette neutral(); + TonalPalette neutral_variant(); + TonalPalette error(); + + private: + CorePalette(double hue, double chroma, bool is_content); + + TonalPalette primary_; + TonalPalette secondary_; + TonalPalette tertiary_; + TonalPalette neutral_; + TonalPalette neutral_variant_; + TonalPalette error_; +}; + +} // namespace material_color_utilities + +#endif // CPP_PALETTES_CORE_H_ diff --git a/third_party/material-color/cpp/palettes/core_test.cc b/third_party/material-color/cpp/palettes/core_test.cc new file mode 100644 index 0000000..ffcdb0a --- /dev/null +++ b/third_party/material-color/cpp/palettes/core_test.cc @@ -0,0 +1,70 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/palettes/core.h" + +#include "testing/base/public/gunit.h" +#include "cpp/cam/cam.h" +#include "cpp/palettes/tones.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { + +TEST(TonesTest, HueRotatesRed) { + int color = 0xffff0000; + + CorePalette palette = CorePalette::Of(color); + + double delta_hue = DiffDegrees(CamFromInt(palette.tertiary().get(50)).hue, + CamFromInt(palette.primary().get(50)).hue); + ASSERT_NEAR(delta_hue, 60.0, 2.0); +} + +TEST(TonesTest, HueRotatesGreen) { + int color = 0xff00ff00; + + CorePalette palette = CorePalette::Of(color); + + double delta_hue = DiffDegrees(CamFromInt(palette.tertiary().get(50)).hue, + CamFromInt(palette.primary().get(50)).hue); + ASSERT_NEAR(delta_hue, 60.0, 2.0); +} + +TEST(TonesTest, HueRotatesBlue) { + int color = 0xff0000ff; + + CorePalette palette = CorePalette::Of(color); + + double delta_hue = DiffDegrees(CamFromInt(palette.tertiary().get(50)).hue, + CamFromInt(palette.primary().get(50)).hue); + ASSERT_NEAR(delta_hue, 60.0, 1.0); +} + +TEST(TonesTest, HueWrapsWhenRotating) { + Cam cam = CamFromInt(IntFromHcl(350, 48, 50)); + + CorePalette palette = CorePalette::Of(cam.hue, cam.chroma); + + double a1_hue = CamFromInt(palette.primary().get(50)).hue; + double a3_hue = CamFromInt(palette.tertiary().get(50)).hue; + ASSERT_NEAR(DiffDegrees(a1_hue, a3_hue), 60.0, 1.0); + ASSERT_NEAR(a3_hue, 50, 1.0); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/palettes/tones.cc b/third_party/material-color/cpp/palettes/tones.cc new file mode 100644 index 0000000..bfd4a14 --- /dev/null +++ b/third_party/material-color/cpp/palettes/tones.cc @@ -0,0 +1,91 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/palettes/tones.h" + +#include + +#include "cpp/cam/cam.h" +#include "cpp/cam/hct.h" + +namespace material_color_utilities { + +TonalPalette::TonalPalette(Argb argb) : key_color_(0.0, 0.0, 0.0) { + Cam cam = CamFromInt(argb); + hue_ = cam.hue; + chroma_ = cam.chroma; + key_color_ = createKeyColor(cam.hue, cam.chroma); +} + +TonalPalette::TonalPalette(Hct hct) + : key_color_(hct.get_hue(), hct.get_chroma(), hct.get_tone()) { + hue_ = hct.get_hue(); + chroma_ = hct.get_chroma(); +} + +TonalPalette::TonalPalette(double hue, double chroma) + : key_color_(hue, chroma, 0.0) { + hue_ = hue; + chroma_ = chroma; + key_color_ = createKeyColor(hue, chroma); +} + +TonalPalette::TonalPalette(double hue, double chroma, Hct key_color) + : key_color_(key_color.get_hue(), key_color.get_chroma(), + key_color.get_tone()) { + hue_ = hue; + chroma_ = chroma; +} + +Argb TonalPalette::get(double tone) const { + return IntFromHcl(hue_, chroma_, tone); +} + +Hct TonalPalette::createKeyColor(double hue, double chroma) { + double start_tone = 50.0; + Hct smallest_delta_hct(hue, chroma, start_tone); + double smallest_delta = abs(smallest_delta_hct.get_chroma() - chroma); + // Starting from T50, check T+/-delta to see if they match the requested + // chroma. + // + // Starts from T50 because T50 has the most chroma available, on + // average. Thus it is most likely to have a direct answer and minimize + // iteration. + for (double delta = 1.0; delta < 50.0; delta += 1.0) { + // Termination condition rounding instead of minimizing delta to avoid + // case where requested chroma is 16.51, and the closest chroma is 16.49. + // Error is minimized, but when rounded and displayed, requested chroma + // is 17, key color's chroma is 16. + if (round(chroma) == round(smallest_delta_hct.get_chroma())) { + return smallest_delta_hct; + } + Hct hct_add(hue, chroma, start_tone + delta); + double hct_add_delta = abs(hct_add.get_chroma() - chroma); + if (hct_add_delta < smallest_delta) { + smallest_delta = hct_add_delta; + smallest_delta_hct = hct_add; + } + Hct hct_subtract(hue, chroma, start_tone - delta); + double hct_subtract_delta = abs(hct_subtract.get_chroma() - chroma); + if (hct_subtract_delta < smallest_delta) { + smallest_delta = hct_subtract_delta; + smallest_delta_hct = hct_subtract; + } + } + return smallest_delta_hct; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/palettes/tones.h b/third_party/material-color/cpp/palettes/tones.h new file mode 100644 index 0000000..71448bf --- /dev/null +++ b/third_party/material-color/cpp/palettes/tones.h @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_PALETTES_TONES_H_ +#define CPP_PALETTES_TONES_H_ + +#include "cpp/cam/hct.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +class TonalPalette { + public: + explicit TonalPalette(Argb argb); + TonalPalette(Hct hct); + TonalPalette(double hue, double chroma); + TonalPalette(double hue, double chroma, Hct key_color); + + /** + * Returns the color for a given tone in this palette. + * + * @param tone 0.0 <= tone <= 100.0 + * @return a color as an integer, in ARGB format. + */ + Argb get(double tone) const; + + double get_hue() const { return hue_; } + double get_chroma() const { return chroma_; } + Hct get_key_color() const { return key_color_; } + + private: + double hue_; + double chroma_; + Hct key_color_; + + Hct createKeyColor(double hue, double chroma); +}; + +} // namespace material_color_utilities +#endif // CPP_PALETTES_TONES_H_ diff --git a/third_party/material-color/cpp/palettes/tones_test.cc b/third_party/material-color/cpp/palettes/tones_test.cc new file mode 100644 index 0000000..bf419ba --- /dev/null +++ b/third_party/material-color/cpp/palettes/tones_test.cc @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/palettes/tones.h" + +#include "testing/base/public/gunit.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { + +TEST(TonesTest, Blue) { + Argb color = 0xff0000ff; + TonalPalette tonal_palette = TonalPalette(color); + EXPECT_EQ(HexFromArgb(tonal_palette.get(100)), "ffffffff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(95)), "fff1efff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(90)), "ffe0e0ff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(80)), "ffbec2ff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(70)), "ff9da3ff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(60)), "ff7c84ff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(50)), "ff5a64ff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(40)), "ff343dff"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(30)), "ff0000ef"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(20)), "ff0001ac"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(10)), "ff00006e"); + EXPECT_EQ(HexFromArgb(tonal_palette.get(0)), "ff000000"); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/celebi.cc b/third_party/material-color/cpp/quantize/celebi.cc new file mode 100644 index 0000000..857979e --- /dev/null +++ b/third_party/material-color/cpp/quantize/celebi.cc @@ -0,0 +1,60 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/celebi.h" + +#include +#include +#include +#include + +#include "cpp/quantize/wsmeans.h" +#include "cpp/quantize/wu.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +QuantizerResult QuantizeCelebi(const std::vector& pixels, + uint16_t max_colors) { + if (max_colors == 0 || pixels.empty()) { + return QuantizerResult(); + } + + if (max_colors > 256) { + max_colors = 256; + } + + int pixel_count = pixels.size(); + + std::vector opaque_pixels; + opaque_pixels.reserve(pixel_count); + for (int i = 0; i < pixel_count; i++) { + int pixel = pixels[i]; + if (!IsOpaque(pixel)) { + continue; + } + opaque_pixels.push_back(pixel); + } + + std::vector wu_result = QuantizeWu(opaque_pixels, max_colors); + + QuantizerResult result = + QuantizeWsmeans(opaque_pixels, wu_result, max_colors); + + return result; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/celebi.h b/third_party/material-color/cpp/quantize/celebi.h new file mode 100644 index 0000000..f2f9d57 --- /dev/null +++ b/third_party/material-color/cpp/quantize/celebi.h @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_QUANTIZE_CELEBI_H_ +#define CPP_QUANTIZE_CELEBI_H_ + +#include +#include + +#include + +#include "cpp/quantize/wsmeans.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +QuantizerResult QuantizeCelebi(const std::vector& pixels, + uint16_t max_colors); + +} // namespace material_color_utilities + +#endif // CPP_QUANTIZE_CELEBI_H_ diff --git a/third_party/material-color/cpp/quantize/celebi_test.cc b/third_party/material-color/cpp/quantize/celebi_test.cc new file mode 100644 index 0000000..a7ad55d --- /dev/null +++ b/third_party/material-color/cpp/quantize/celebi_test.cc @@ -0,0 +1,131 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/celebi.h" + +#include +#include + +#include "testing/base/public/gunit.h" + +namespace material_color_utilities { + +namespace { + +TEST(CelebiTest, FullImage) { + std::vector pixels(12544); + for (size_t i = 0; i < pixels.size(); i++) { + // Creates 128 distinct colors + pixels[i] = i % 8000; + } + + int iterations = 1; + uint16_t max_colors = 128; + double sum = 0; + + for (int i = 0; i < iterations; i++) { + clock_t begin = clock(); + QuantizeCelebi(pixels, max_colors); + clock_t end = clock(); + double time_spent = static_cast(end - begin) / CLOCKS_PER_SEC; + sum += time_spent; + } +} + +TEST(CelebiTest, OneRed) { + std::vector pixels; + pixels.push_back(0xffff0000); + QuantizerResult result = QuantizeCelebi(pixels, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xffff0000], 1); +} + +TEST(CelebiTest, OneGreen) { + std::vector pixels; + pixels.push_back(0xff00ff00); + QuantizerResult result = QuantizeCelebi(pixels, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff00ff00], 1); +} + +TEST(CelebiTest, OneBlue) { + std::vector pixels; + pixels.push_back(0xff0000ff); + QuantizerResult result = QuantizeCelebi(pixels, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff0000ff], 1); +} + +TEST(CelebiTest, FiveBlue) { + std::vector pixels; + for (int i = 0; i < 5; i++) { + pixels.push_back(0xff0000ff); + } + QuantizerResult result = QuantizeCelebi(pixels, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff0000ff], 5); +} + +TEST(CelebiTest, OneRedOneGreenOneBlue) { + std::vector pixels; + pixels.push_back(0xffff0000); + pixels.push_back(0xff00ff00); + pixels.push_back(0xff0000ff); + QuantizerResult result = QuantizeCelebi(pixels, 256); + EXPECT_EQ(result.color_to_count.size(), 3u); + EXPECT_EQ(result.color_to_count[0xffff0000], 1); + EXPECT_EQ(result.color_to_count[0xff00ff00], 1); + EXPECT_EQ(result.color_to_count[0xff0000ff], 1); +} + +TEST(CelebiTest, TwoRedThreeGreen) { + std::vector pixels; + pixels.push_back(0xffff0000); + pixels.push_back(0xffff0000); + pixels.push_back(0xff00ff00); + pixels.push_back(0xff00ff00); + pixels.push_back(0xff00ff00); + QuantizerResult result = QuantizeCelebi(pixels, 256); + EXPECT_EQ(result.color_to_count.size(), 2u); + EXPECT_EQ(result.color_to_count[0xffff0000], 2); + EXPECT_EQ(result.color_to_count[0xff00ff00], 3); +} + +TEST(CelebiTest, NoColors) { + std::vector pixels; + pixels.push_back(0xFFFFFFFF); + QuantizerResult result = QuantizeCelebi(pixels, 0); + EXPECT_TRUE(result.color_to_count.empty()); + EXPECT_TRUE(result.input_pixel_to_cluster_pixel.empty()); +} + +TEST(CelebiTest, SingleTransparent) { + std::vector pixels; + pixels.push_back(0x20F93013); + QuantizerResult result = QuantizeCelebi(pixels, 1); + EXPECT_TRUE(result.color_to_count.empty()); + EXPECT_TRUE(result.input_pixel_to_cluster_pixel.empty()); +} + +TEST(CelebiTest, TooManyColors) { + std::vector pixels; + QuantizerResult result = QuantizeCelebi(pixels, 32767); + EXPECT_TRUE(result.color_to_count.empty()); + EXPECT_TRUE(result.input_pixel_to_cluster_pixel.empty()); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/lab.cc b/third_party/material-color/cpp/quantize/lab.cc new file mode 100644 index 0000000..caa575b --- /dev/null +++ b/third_party/material-color/cpp/quantize/lab.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/lab.h" + +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +Argb IntFromLab(const Lab lab) { + double e = 216.0 / 24389.0; + double kappa = 24389.0 / 27.0; + double ke = 8.0; + + double fy = (lab.l + 16.0) / 116.0; + double fx = (lab.a / 500.0) + fy; + double fz = fy - (lab.b / 200.0); + double fx3 = fx * fx * fx; + double x_normalized = (fx3 > e) ? fx3 : (116.0 * fx - 16.0) / kappa; + double y_normalized = (lab.l > ke) ? fy * fy * fy : (lab.l / kappa); + double fz3 = fz * fz * fz; + double z_normalized = (fz3 > e) ? fz3 : (116.0 * fz - 16.0) / kappa; + double x = x_normalized * kWhitePointD65[0]; + double y = y_normalized * kWhitePointD65[1]; + double z = z_normalized * kWhitePointD65[2]; + + // intFromXyz + double rL = 3.2406 * x - 1.5372 * y - 0.4986 * z; + double gL = -0.9689 * x + 1.8758 * y + 0.0415 * z; + double bL = 0.0557 * x - 0.2040 * y + 1.0570 * z; + + int red = Delinearized(rL); + int green = Delinearized(gL); + int blue = Delinearized(bL); + + return ArgbFromRgb(red, green, blue); +} + +Lab LabFromInt(const Argb argb) { + int red = (argb & 0x00ff0000) >> 16; + int green = (argb & 0x0000ff00) >> 8; + int blue = (argb & 0x000000ff); + double red_l = Linearized(red); + double green_l = Linearized(green); + double blue_l = Linearized(blue); + double x = 0.41233895 * red_l + 0.35762064 * green_l + 0.18051042 * blue_l; + double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l; + double z = 0.01932141 * red_l + 0.11916382 * green_l + 0.95034478 * blue_l; + double y_normalized = y / kWhitePointD65[1]; + double e = 216.0 / 24389.0; + double kappa = 24389.0 / 27.0; + double fy; + if (y_normalized > e) { + fy = pow(y_normalized, 1.0 / 3.0); + } else { + fy = (kappa * y_normalized + 16) / 116; + } + + double x_normalized = x / kWhitePointD65[0]; + double fx; + if (x_normalized > e) { + fx = pow(x_normalized, 1.0 / 3.0); + } else { + fx = (kappa * x_normalized + 16) / 116; + } + + double z_normalized = z / kWhitePointD65[2]; + double fz; + if (z_normalized > e) { + fz = pow(z_normalized, 1.0 / 3.0); + } else { + fz = (kappa * z_normalized + 16) / 116; + } + + double l = 116.0 * fy - 16; + double a = 500.0 * (fx - fy); + double b = 200.0 * (fy - fz); + return {l, a, b}; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/lab.h b/third_party/material-color/cpp/quantize/lab.h new file mode 100644 index 0000000..b983c73 --- /dev/null +++ b/third_party/material-color/cpp/quantize/lab.h @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_QUANTIZE_LAB_H_ +#define CPP_QUANTIZE_LAB_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +struct Lab { + double l = 0.0; + double a = 0.0; + double b = 0.0; + + double DeltaE(const Lab& lab) { + double d_l = l - lab.l; + double d_a = a - lab.a; + double d_b = b - lab.b; + return (d_l * d_l) + (d_a * d_a) + (d_b * d_b); + } + + std::string ToString() { + return "Lab: L* " + std::to_string(l) + " a* " + std::to_string(a) + + " b* " + std::to_string(b); + } +}; + +Argb IntFromLab(const Lab lab); +Lab LabFromInt(const Argb argb); + +} // namespace material_color_utilities +#endif // CPP_QUANTIZE_LAB_H_ diff --git a/third_party/material-color/cpp/quantize/wsmeans.cc b/third_party/material-color/cpp/quantize/wsmeans.cc new file mode 100644 index 0000000..92ad860 --- /dev/null +++ b/third_party/material-color/cpp/quantize/wsmeans.cc @@ -0,0 +1,266 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/wsmeans.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "cpp/quantize/lab.h" + +constexpr int kMaxIterations = 100; +constexpr double kMinDeltaE = 3.0; + +namespace material_color_utilities { + +struct Swatch { + Argb argb = 0; + int population = 0; + + bool operator<(const Swatch& b) const { return population > b.population; } +}; + +struct DistanceToIndex { + double distance = 0.0; + int index = 0; + + bool operator<(const DistanceToIndex& a) const { + return distance < a.distance; + } +}; + +QuantizerResult QuantizeWsmeans(const std::vector& input_pixels, + const std::vector& starting_clusters, + uint16_t max_colors) { + if (max_colors == 0 || input_pixels.empty()) { + return QuantizerResult(); + } + + if (max_colors > 256) { + // If colors is outside the range, just set it the max. + max_colors = 256; + } + + uint32_t pixel_count = input_pixels.size(); + absl::flat_hash_map pixel_to_count; + std::vector pixels; + pixels.reserve(pixel_count); + std::vector points; + points.reserve(pixel_count); + for (Argb pixel : input_pixels) { + // tested over 1000 runs with 128 colors, 12544 (112 x 112) + // std::map 10.9 ms + // std::unordered_map 10.2 ms + // absl::btree_map 9.0 ms + // absl::flat_hash_map 8.0 ms + absl::flat_hash_map::iterator it = pixel_to_count.find(pixel); + if (it != pixel_to_count.end()) { + it->second++; + + } else { + pixels.push_back(pixel); + points.push_back(LabFromInt(pixel)); + pixel_to_count[pixel] = 1; + } + } + + int cluster_count = std::min((int)max_colors, (int)points.size()); + + if (!starting_clusters.empty()) { + cluster_count = std::min(cluster_count, (int)starting_clusters.size()); + } + + int pixel_count_sums[256] = {}; + std::vector clusters; + clusters.reserve(starting_clusters.size()); + for (int argb : starting_clusters) { + clusters.push_back(LabFromInt(argb)); + } + + srand(42688); + int additional_clusters_needed = cluster_count - clusters.size(); + if (starting_clusters.empty() && additional_clusters_needed > 0) { + for (int i = 0; i < additional_clusters_needed; i++) { + // Adds a random Lab color to clusters. + double l = rand() / (static_cast(RAND_MAX)) * (100.0) + 0.0; + double a = + rand() / (static_cast(RAND_MAX)) * (100.0 - -100.0) - 100.0; + double b = + rand() / (static_cast(RAND_MAX)) * (100.0 - -100.0) - 100.0; + clusters.push_back({l, a, b}); + } + } + + std::vector cluster_indices; + cluster_indices.reserve(points.size()); + + srand(42688); + for (size_t i = 0; i < points.size(); i++) { + cluster_indices.push_back(rand() % cluster_count); + } + + std::vector> index_matrix( + cluster_count, std::vector(cluster_count, 0)); + + std::vector> distance_to_index_matrix( + cluster_count, std::vector(cluster_count)); + + for (int iteration = 0; iteration < kMaxIterations; iteration++) { + // Calculate cluster distances + for (int i = 0; i < cluster_count; i++) { + distance_to_index_matrix[i][i].distance = 0; + distance_to_index_matrix[i][i].index = i; + for (int j = i + 1; j < cluster_count; j++) { + double distance = clusters[i].DeltaE(clusters[j]); + + distance_to_index_matrix[j][i].distance = distance; + distance_to_index_matrix[j][i].index = i; + distance_to_index_matrix[i][j].distance = distance; + distance_to_index_matrix[i][j].index = j; + } + + std::vector row = distance_to_index_matrix[i]; + std::sort(row.begin(), row.end()); + + for (int j = 0; j < cluster_count; j++) { + index_matrix[i][j] = row[j].index; + } + } + + // Reassign points + bool color_moved = false; + for (size_t i = 0; i < points.size(); i++) { + Lab point = points[i]; + + int previous_cluster_index = cluster_indices[i]; + Lab previous_cluster = clusters[previous_cluster_index]; + double previous_distance = point.DeltaE(previous_cluster); + double minimum_distance = previous_distance; + int new_cluster_index = -1; + + for (int j = 0; j < cluster_count; j++) { + if (distance_to_index_matrix[previous_cluster_index][j].distance >= + 4 * previous_distance) { + continue; + } + double distance = point.DeltaE(clusters[j]); + if (distance < minimum_distance) { + minimum_distance = distance; + new_cluster_index = j; + } + } + if (new_cluster_index != -1) { + double distanceChange = + abs(sqrt(minimum_distance) - sqrt(previous_distance)); + if (distanceChange > kMinDeltaE) { + color_moved = true; + cluster_indices[i] = new_cluster_index; + } + } + } + + if (!color_moved && (iteration != 0)) { + break; + } + + // Recalculate cluster centers + double component_a_sums[256] = {}; + double component_b_sums[256] = {}; + double component_c_sums[256] = {}; + for (int i = 0; i < cluster_count; i++) { + pixel_count_sums[i] = 0; + } + + for (size_t i = 0; i < points.size(); i++) { + int clusterIndex = cluster_indices[i]; + Lab point = points[i]; + int count = pixel_to_count[pixels[i]]; + + pixel_count_sums[clusterIndex] += count; + component_a_sums[clusterIndex] += (point.l * count); + component_b_sums[clusterIndex] += (point.a * count); + component_c_sums[clusterIndex] += (point.b * count); + } + + for (int i = 0; i < cluster_count; i++) { + int count = pixel_count_sums[i]; + if (count == 0) { + clusters[i] = {0, 0, 0}; + continue; + } + double a = component_a_sums[i] / count; + double b = component_b_sums[i] / count; + double c = component_c_sums[i] / count; + clusters[i] = {a, b, c}; + } + } + + std::vector swatches; + std::vector cluster_argbs; + std::vector all_cluster_argbs; + for (int i = 0; i < cluster_count; i++) { + Argb possible_new_cluster = IntFromLab(clusters[i]); + all_cluster_argbs.push_back(possible_new_cluster); + + int count = pixel_count_sums[i]; + if (count == 0) { + continue; + } + int use_new_cluster = 1; + for (size_t j = 0; j < swatches.size(); j++) { + if (swatches[j].argb == possible_new_cluster) { + swatches[j].population += count; + use_new_cluster = 0; + break; + } + } + + if (use_new_cluster == 0) { + continue; + } + cluster_argbs.push_back(possible_new_cluster); + swatches.push_back({possible_new_cluster, count}); + } + std::sort(swatches.begin(), swatches.end()); + + // Constructs the quantizer result to return. + + std::map color_to_count; + for (size_t i = 0; i < swatches.size(); i++) { + color_to_count[swatches[i].argb] = swatches[i].population; + } + + std::map input_pixel_to_cluster_pixel; + for (size_t i = 0; i < points.size(); i++) { + int pixel = pixels[i]; + int cluster_index = cluster_indices[i]; + int cluster_argb = all_cluster_argbs[cluster_index]; + input_pixel_to_cluster_pixel[pixel] = cluster_argb; + } + + return {color_to_count, input_pixel_to_cluster_pixel}; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/wsmeans.h b/third_party/material-color/cpp/quantize/wsmeans.h new file mode 100644 index 0000000..72aea72 --- /dev/null +++ b/third_party/material-color/cpp/quantize/wsmeans.h @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_QUANTIZE_WSMEANS_H_ +#define CPP_QUANTIZE_WSMEANS_H_ +#include + +#include +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +struct QuantizerResult { + std::map color_to_count; + std::map input_pixel_to_cluster_pixel; +}; + +QuantizerResult QuantizeWsmeans(const std::vector& input_pixels, + const std::vector& starting_clusters, + uint16_t max_colors); +} // namespace material_color_utilities + +#endif // CPP_QUANTIZE_WSMEANS_H_ diff --git a/third_party/material-color/cpp/quantize/wsmeans_test.cc b/third_party/material-color/cpp/quantize/wsmeans_test.cc new file mode 100644 index 0000000..901cdeb --- /dev/null +++ b/third_party/material-color/cpp/quantize/wsmeans_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/wsmeans.h" + +#include + +#include "testing/base/public/gunit.h" + +namespace material_color_utilities { + +namespace { +TEST(WsmeansTest, FullImage) { + std::vector pixels(12544); + for (size_t i = 0; i < pixels.size(); i++) { + // Creates 128 distinct colors + pixels[i] = i % 8000; + } + std::vector starting_clusters; + + int iterations = 1; + int max_colors = 128; + + double sum = 0; + + for (int i = 0; i < iterations; i++) { + clock_t begin = clock(); + QuantizeWsmeans(pixels, starting_clusters, max_colors); + clock_t end = clock(); + double time_spent = static_cast(end - begin) / CLOCKS_PER_SEC; + sum += time_spent; + } +} + +TEST(WsmeansTest, OneRedAndO) { + std::vector pixels; + pixels.push_back(0xff141216); + std::vector starting_clusters; + QuantizerResult result = QuantizeWsmeans(pixels, starting_clusters, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff141216], 1); +} + +TEST(WsmeansTest, OneRed) { + std::vector pixels; + pixels.push_back(0xffff0000); + std::vector starting_clusters; + QuantizerResult result = QuantizeWsmeans(pixels, starting_clusters, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xffff0000], 1); +} + +TEST(WsmeansTest, OneGreen) { + std::vector pixels; + pixels.push_back(0xff00ff00); + std::vector starting_clusters; + QuantizerResult result = QuantizeWsmeans(pixels, starting_clusters, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff00ff00], 1); +} + +TEST(WsmeansTest, OneBlue) { + std::vector pixels; + pixels.push_back(0xff0000ff); + std::vector starting_clusters; + QuantizerResult result = QuantizeWsmeans(pixels, starting_clusters, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff0000ff], 1); +} + +TEST(WsmeansTest, FiveBlue) { + std::vector pixels; + for (int i = 0; i < 5; i++) { + pixels.push_back(0xff0000ff); + } + std::vector starting_clusters; + QuantizerResult result = QuantizeWsmeans(pixels, starting_clusters, 256); + EXPECT_EQ(result.color_to_count.size(), 1u); + EXPECT_EQ(result.color_to_count[0xff0000ff], 5); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/wu.cc b/third_party/material-color/cpp/quantize/wu.cc new file mode 100644 index 0000000..fad0ecf --- /dev/null +++ b/third_party/material-color/cpp/quantize/wu.cc @@ -0,0 +1,360 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/wu.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +struct Box { + int r0 = 0; + int r1 = 0; + int g0 = 0; + int g1 = 0; + int b0 = 0; + int b1 = 0; + int vol = 0; +}; + +enum class Direction { + kRed, + kGreen, + kBlue, +}; + +constexpr int kIndexBits = 5; +constexpr int kIndexCount = ((1 << kIndexBits) + 1); +constexpr int kTotalSize = (kIndexCount * kIndexCount * kIndexCount); +constexpr int kMaxColors = 256; + +using IntArray = std::array; +using DoubleArray = std::array; + +int GetIndex(int r, int g, int b) { + return (r << (kIndexBits * 2)) + (r << (kIndexBits + 1)) + (g << kIndexBits) + + r + g + b; +} + +void ConstructHistogram(const std::vector& pixels, IntArray& weights, + IntArray& m_r, IntArray& m_g, IntArray& m_b, + DoubleArray& moments) { + for (size_t i = 0; i < pixels.size(); i++) { + Argb pixel = pixels[i]; + int red = RedFromInt(pixel); + int green = GreenFromInt(pixel); + int blue = BlueFromInt(pixel); + + int bits_to_remove = 8 - kIndexBits; + int index_r = (red >> bits_to_remove) + 1; + int index_g = (green >> bits_to_remove) + 1; + int index_b = (blue >> bits_to_remove) + 1; + int index = GetIndex(index_r, index_g, index_b); + + weights[index]++; + m_r[index] += red; + m_g[index] += green; + m_b[index] += blue; + moments[index] += (red * red) + (green * green) + (blue * blue); + } +} + +void ComputeMoments(IntArray& weights, IntArray& m_r, IntArray& m_g, + IntArray& m_b, DoubleArray& moments) { + for (int r = 1; r < kIndexCount; r++) { + int64_t area[kIndexCount] = {}; + int64_t area_r[kIndexCount] = {}; + int64_t area_g[kIndexCount] = {}; + int64_t area_b[kIndexCount] = {}; + double area_2[kIndexCount] = {}; + for (int g = 1; g < kIndexCount; g++) { + int64_t line = 0; + int64_t line_r = 0; + int64_t line_g = 0; + int64_t line_b = 0; + double line_2 = 0.0; + for (int b = 1; b < kIndexCount; b++) { + int index = GetIndex(r, g, b); + line += weights[index]; + line_r += m_r[index]; + line_g += m_g[index]; + line_b += m_b[index]; + line_2 += moments[index]; + + area[b] += line; + area_r[b] += line_r; + area_g[b] += line_g; + area_b[b] += line_b; + area_2[b] += line_2; + + int previous_index = GetIndex(r - 1, g, b); + weights[index] = weights[previous_index] + area[b]; + m_r[index] = m_r[previous_index] + area_r[b]; + m_g[index] = m_g[previous_index] + area_g[b]; + m_b[index] = m_b[previous_index] + area_b[b]; + moments[index] = moments[previous_index] + area_2[b]; + } + } + } +} + +int64_t Top(const Box& cube, const Direction direction, const int position, + const IntArray& moment) { + if (direction == Direction::kRed) { + return (moment[GetIndex(position, cube.g1, cube.b1)] - + moment[GetIndex(position, cube.g1, cube.b0)] - + moment[GetIndex(position, cube.g0, cube.b1)] + + moment[GetIndex(position, cube.g0, cube.b0)]); + } else if (direction == Direction::kGreen) { + return (moment[GetIndex(cube.r1, position, cube.b1)] - + moment[GetIndex(cube.r1, position, cube.b0)] - + moment[GetIndex(cube.r0, position, cube.b1)] + + moment[GetIndex(cube.r0, position, cube.b0)]); + } else { + return (moment[GetIndex(cube.r1, cube.g1, position)] - + moment[GetIndex(cube.r1, cube.g0, position)] - + moment[GetIndex(cube.r0, cube.g1, position)] + + moment[GetIndex(cube.r0, cube.g0, position)]); + } +} + +int64_t Bottom(const Box& cube, const Direction direction, + const IntArray& moment) { + if (direction == Direction::kRed) { + return (-moment[GetIndex(cube.r0, cube.g1, cube.b1)] + + moment[GetIndex(cube.r0, cube.g1, cube.b0)] + + moment[GetIndex(cube.r0, cube.g0, cube.b1)] - + moment[GetIndex(cube.r0, cube.g0, cube.b0)]); + } else if (direction == Direction::kGreen) { + return (-moment[GetIndex(cube.r1, cube.g0, cube.b1)] + + moment[GetIndex(cube.r1, cube.g0, cube.b0)] + + moment[GetIndex(cube.r0, cube.g0, cube.b1)] - + moment[GetIndex(cube.r0, cube.g0, cube.b0)]); + } else { + return (-moment[GetIndex(cube.r1, cube.g1, cube.b0)] + + moment[GetIndex(cube.r1, cube.g0, cube.b0)] + + moment[GetIndex(cube.r0, cube.g1, cube.b0)] - + moment[GetIndex(cube.r0, cube.g0, cube.b0)]); + } +} + +int64_t Vol(const Box& cube, const IntArray& moment) { + return (moment[GetIndex(cube.r1, cube.g1, cube.b1)] - + moment[GetIndex(cube.r1, cube.g1, cube.b0)] - + moment[GetIndex(cube.r1, cube.g0, cube.b1)] + + moment[GetIndex(cube.r1, cube.g0, cube.b0)] - + moment[GetIndex(cube.r0, cube.g1, cube.b1)] + + moment[GetIndex(cube.r0, cube.g1, cube.b0)] + + moment[GetIndex(cube.r0, cube.g0, cube.b1)] - + moment[GetIndex(cube.r0, cube.g0, cube.b0)]); +} + +double Variance(const Box& cube, const IntArray& weights, const IntArray& m_r, + const IntArray& m_g, const IntArray& m_b, + const DoubleArray& moments) { + double dr = Vol(cube, m_r); + double dg = Vol(cube, m_g); + double db = Vol(cube, m_b); + double xx = moments[GetIndex(cube.r1, cube.g1, cube.b1)] - + moments[GetIndex(cube.r1, cube.g1, cube.b0)] - + moments[GetIndex(cube.r1, cube.g0, cube.b1)] + + moments[GetIndex(cube.r1, cube.g0, cube.b0)] - + moments[GetIndex(cube.r0, cube.g1, cube.b1)] + + moments[GetIndex(cube.r0, cube.g1, cube.b0)] + + moments[GetIndex(cube.r0, cube.g0, cube.b1)] - + moments[GetIndex(cube.r0, cube.g0, cube.b0)]; + double hypotenuse = dr * dr + dg * dg + db * db; + double volume = Vol(cube, weights); + return xx - hypotenuse / volume; +} + +double Maximize(const Box& cube, const Direction direction, const int first, + const int last, int* cut, const int64_t whole_w, + const int64_t whole_r, const int64_t whole_g, + const int64_t whole_b, const IntArray& weights, + const IntArray& m_r, const IntArray& m_g, const IntArray& m_b) { + int64_t bottom_r = Bottom(cube, direction, m_r); + int64_t bottom_g = Bottom(cube, direction, m_g); + int64_t bottom_b = Bottom(cube, direction, m_b); + int64_t bottom_w = Bottom(cube, direction, weights); + + double max = 0.0; + *cut = -1; + + int64_t half_r, half_g, half_b, half_w; + for (int i = first; i < last; i++) { + half_r = bottom_r + Top(cube, direction, i, m_r); + half_g = bottom_g + Top(cube, direction, i, m_g); + half_b = bottom_b + Top(cube, direction, i, m_b); + half_w = bottom_w + Top(cube, direction, i, weights); + if (half_w == 0) { + continue; + } + + double temp = (static_cast(half_r) * half_r + + static_cast(half_g) * half_g + + static_cast(half_b) * half_b) / + static_cast(half_w); + + half_r = whole_r - half_r; + half_g = whole_g - half_g; + half_b = whole_b - half_b; + half_w = whole_w - half_w; + if (half_w == 0) { + continue; + } + temp += (static_cast(half_r) * half_r + + static_cast(half_g) * half_g + + static_cast(half_b) * half_b) / + static_cast(half_w); + + if (temp > max) { + max = temp; + *cut = i; + } + } + return max; +} + +bool Cut(Box& box1, Box& box2, const IntArray& weights, const IntArray& m_r, + const IntArray& m_g, const IntArray& m_b) { + int64_t whole_r = Vol(box1, m_r); + int64_t whole_g = Vol(box1, m_g); + int64_t whole_b = Vol(box1, m_b); + int64_t whole_w = Vol(box1, weights); + + int cut_r, cut_g, cut_b; + double max_r = + Maximize(box1, Direction::kRed, box1.r0 + 1, box1.r1, &cut_r, whole_w, + whole_r, whole_g, whole_b, weights, m_r, m_g, m_b); + double max_g = + Maximize(box1, Direction::kGreen, box1.g0 + 1, box1.g1, &cut_g, whole_w, + whole_r, whole_g, whole_b, weights, m_r, m_g, m_b); + double max_b = + Maximize(box1, Direction::kBlue, box1.b0 + 1, box1.b1, &cut_b, whole_w, + whole_r, whole_g, whole_b, weights, m_r, m_g, m_b); + + Direction direction; + if (max_r >= max_g && max_r >= max_b) { + direction = Direction::kRed; + if (cut_r < 0) { + return false; + } + } else if (max_g >= max_r && max_g >= max_b) { + direction = Direction::kGreen; + } else { + direction = Direction::kBlue; + } + + box2.r1 = box1.r1; + box2.g1 = box1.g1; + box2.b1 = box1.b1; + + if (direction == Direction::kRed) { + box2.r0 = box1.r1 = cut_r; + box2.g0 = box1.g0; + box2.b0 = box1.b0; + } else if (direction == Direction::kGreen) { + box2.r0 = box1.r0; + box2.g0 = box1.g1 = cut_g; + box2.b0 = box1.b0; + } else { + box2.r0 = box1.r0; + box2.g0 = box1.g0; + box2.b0 = box1.b1 = cut_b; + } + + box1.vol = (box1.r1 - box1.r0) * (box1.g1 - box1.g0) * (box1.b1 - box1.b0); + box2.vol = (box2.r1 - box2.r0) * (box2.g1 - box2.g0) * (box2.b1 - box2.b0); + return true; +} + +std::vector QuantizeWu(const std::vector& pixels, + uint16_t max_colors) { + if (max_colors <= 0 || max_colors > 256 || pixels.empty()) { + return std::vector(); + } + + IntArray weights = {}; + IntArray moments_red = {}; + IntArray moments_green = {}; + IntArray moments_blue = {}; + DoubleArray moments = {}; + ConstructHistogram(pixels, weights, moments_red, moments_green, moments_blue, + moments); + ComputeMoments(weights, moments_red, moments_green, moments_blue, moments); + + std::vector cubes(kMaxColors); + cubes[0].r0 = cubes[0].g0 = cubes[0].b0 = 0; + cubes[0].r1 = cubes[0].g1 = cubes[0].b1 = kIndexCount - 1; + + std::vector volume_variance(kMaxColors); + int next = 0; + for (int i = 1; i < max_colors; ++i) { + if (Cut(cubes[next], cubes[i], weights, moments_red, moments_green, + moments_blue)) { + volume_variance[next] = + cubes[next].vol > 1 ? Variance(cubes[next], weights, moments_red, + moments_green, moments_blue, moments) + : 0.0; + volume_variance[i] = cubes[i].vol > 1 + ? Variance(cubes[i], weights, moments_red, + moments_green, moments_blue, moments) + : 0.0; + } else { + volume_variance[next] = 0.0; + i--; + } + + next = 0; + double temp = volume_variance[0]; + for (int j = 1; j <= i; j++) { + if (volume_variance[j] > temp) { + temp = volume_variance[j]; + next = j; + } + } + if (temp <= 0.0) { + max_colors = i + 1; + break; + } + } + + std::vector out_colors; + for (int i = 0; i < max_colors; ++i) { + int64_t weight = Vol(cubes[i], weights); + if (weight > 0) { + int32_t red = Vol(cubes[i], moments_red) / weight; + int32_t green = Vol(cubes[i], moments_green) / weight; + int32_t blue = Vol(cubes[i], moments_blue) / weight; + uint32_t argb = ArgbFromRgb(red, green, blue); + out_colors.push_back(argb); + } + } + + return out_colors; +} +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/quantize/wu.h b/third_party/material-color/cpp/quantize/wu.h new file mode 100644 index 0000000..36f496b --- /dev/null +++ b/third_party/material-color/cpp/quantize/wu.h @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_QUANTIZE_WU_H_ +#define CPP_QUANTIZE_WU_H_ + +#include + +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +std::vector QuantizeWu(const std::vector& pixels, + uint16_t max_colors); +} +#endif // CPP_QUANTIZE_WU_H_ diff --git a/third_party/material-color/cpp/quantize/wu_test.cc b/third_party/material-color/cpp/quantize/wu_test.cc new file mode 100644 index 0000000..5341e97 --- /dev/null +++ b/third_party/material-color/cpp/quantize/wu_test.cc @@ -0,0 +1,116 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/quantize/wu.h" + +#include +#include + +#include "testing/base/public/gunit.h" + +namespace material_color_utilities { + +namespace { + +TEST(WuTest, FullImage) { + std::vector pixels(12544); + for (size_t i = 0; i < pixels.size(); i++) { + // Creates 128 distinct colors + pixels[i] = i % 8000; + } + + uint16_t max_colors = 128; + + QuantizeWu(pixels, max_colors); +} + +TEST(WuTest, TwoRedThreeGreen) { + std::vector pixels; + pixels.push_back(0xffff0000); + pixels.push_back(0xffff0000); + pixels.push_back(0xffff0000); + pixels.push_back(0xff00ff00); + pixels.push_back(0xff00ff00); + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 2u); +} + +TEST(WuTest, OneRed) { + std::vector pixels; + pixels.push_back(0xffff0000); + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 0xffff0000); +} + +TEST(WuTest, OneGreen) { + std::vector pixels; + pixels.push_back(0xff00ff00); + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 0xff00ff00); +} + +TEST(WuTest, OneBlue) { + std::vector pixels; + pixels.push_back(0xff0000ff); + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 0xff0000ff); +} + +TEST(WuTest, FiveBlue) { + std::vector pixels; + for (int i = 0; i < 5; i++) { + pixels.push_back(0xff0000ff); + } + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 0xff0000ff); +} + +TEST(WuTest, OneRedAndO) { + std::vector pixels; + pixels.push_back(0xff141216); + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 0xff141216); +} + +TEST(WuTest, RedGreenBlue) { + std::vector pixels; + pixels.push_back(0xffff0000); + pixels.push_back(0xff00ff00); + pixels.push_back(0xff0000ff); + std::vector result = QuantizeWu(pixels, 256); + EXPECT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], 0xff0000ff); + EXPECT_EQ(result[1], 0xffff0000); + EXPECT_EQ(result[2], 0xff00ff00); +} + +TEST(WuTest, Testonly) { + std::vector pixels; + pixels.push_back(0xff010203); + pixels.push_back(0xff665544); + pixels.push_back(0xff708090); + pixels.push_back(0xffc0ffee); + pixels.push_back(0xfffedcba); + std::vector result = QuantizeWu(pixels, 256); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/dynamic_scheme.cc b/third_party/material-color/cpp/scheme/dynamic_scheme.cc new file mode 100644 index 0000000..12f6ef6 --- /dev/null +++ b/third_party/material-color/cpp/scheme/dynamic_scheme.cc @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/dynamic_scheme.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/variant.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +DynamicScheme::DynamicScheme(Argb source_color_argb, Variant variant, + double contrast_level, bool is_dark, + TonalPalette primary_palette, + TonalPalette secondary_palette, + TonalPalette tertiary_palette, + TonalPalette neutral_palette, + TonalPalette neutral_variant_palette) + : source_color_hct(Hct(source_color_argb)), + variant(variant), + is_dark(is_dark), + contrast_level(contrast_level), + primary_palette(primary_palette), + secondary_palette(secondary_palette), + tertiary_palette(tertiary_palette), + neutral_palette(neutral_palette), + neutral_variant_palette(neutral_variant_palette), + error_palette(TonalPalette(25.0, 84.0)) {} + +double DynamicScheme::GetRotatedHue(Hct source_color, std::vector hues, + std::vector rotations) { + double source_hue = source_color.get_hue(); + + if (rotations.size() == 1) { + return SanitizeDegreesDouble(source_color.get_hue() + rotations[0]); + } + int size = hues.size(); + for (int i = 0; i <= (size - 2); i++) { + double this_hue = hues[i]; + double next_hue = hues[i + 1]; + if (this_hue < source_hue && source_hue < next_hue) { + return SanitizeDegreesDouble(source_hue + rotations[i]); + } + } + + return source_hue; +} + +Argb DynamicScheme::SourceColorArgb() const { return source_color_hct.ToInt(); } + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/dynamic_scheme.h b/third_party/material-color/cpp/scheme/dynamic_scheme.h new file mode 100644 index 0000000..8878851 --- /dev/null +++ b/third_party/material-color/cpp/scheme/dynamic_scheme.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_DYNAMIC_SCHEME_H_ +#define CPP_SCHEME_DYNAMIC_SCHEME_H_ + +#include + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +struct DynamicScheme { + Hct source_color_hct; + Variant variant; + bool is_dark; + double contrast_level; + + TonalPalette primary_palette; + TonalPalette secondary_palette; + TonalPalette tertiary_palette; + TonalPalette neutral_palette; + TonalPalette neutral_variant_palette; + TonalPalette error_palette; + + DynamicScheme(Argb source_color_argb, Variant variant, double contrast_level, + bool is_dark, TonalPalette primary_palette, + TonalPalette secondary_palette, TonalPalette tertiary_palette, + TonalPalette neutral_palette, + TonalPalette neutral_variant_palette); + + static double GetRotatedHue(Hct source_color, std::vector hues, + std::vector rotations); + + Argb SourceColorArgb() const; +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_DYNAMIC_SCHEME_H_ diff --git a/third_party/material-color/cpp/scheme/scheme.cc b/third_party/material-color/cpp/scheme/scheme.cc new file mode 100644 index 0000000..a28b5a8 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme.cc @@ -0,0 +1,113 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme.h" + +#include "cpp/palettes/core.h" + +// This file is automatically generated. Do not modify it. + +namespace material_color_utilities { + +Scheme MaterialLightColorSchemeFromPalette(CorePalette palette) { + Scheme scheme = {}; + + scheme.primary = palette.primary().get(40); + scheme.on_primary = palette.primary().get(100); + scheme.primary_container = palette.primary().get(90); + scheme.on_primary_container = palette.primary().get(10); + scheme.secondary = palette.secondary().get(40); + scheme.on_secondary = palette.secondary().get(100); + scheme.secondary_container = palette.secondary().get(90); + scheme.on_secondary_container = palette.secondary().get(10); + scheme.tertiary = palette.tertiary().get(40); + scheme.on_tertiary = palette.tertiary().get(100); + scheme.tertiary_container = palette.tertiary().get(90); + scheme.on_tertiary_container = palette.tertiary().get(10); + scheme.error = palette.error().get(40); + scheme.on_error = palette.error().get(100); + scheme.error_container = palette.error().get(90); + scheme.on_error_container = palette.error().get(10); + scheme.background = palette.neutral().get(99); + scheme.on_background = palette.neutral().get(10); + scheme.surface = palette.neutral().get(99); + scheme.on_surface = palette.neutral().get(10); + scheme.surface_variant = palette.neutral_variant().get(90); + scheme.on_surface_variant = palette.neutral_variant().get(30); + scheme.outline = palette.neutral_variant().get(50); + scheme.outline_variant = palette.neutral_variant().get(80); + scheme.shadow = palette.neutral().get(0); + scheme.scrim = palette.neutral().get(0); + scheme.inverse_surface = palette.neutral().get(20); + scheme.inverse_on_surface = palette.neutral().get(95); + scheme.inverse_primary = palette.primary().get(80); + + return scheme; +} + +Scheme MaterialDarkColorSchemeFromPalette(CorePalette palette) { + Scheme scheme = {}; + + scheme.primary = palette.primary().get(80); + scheme.on_primary = palette.primary().get(20); + scheme.primary_container = palette.primary().get(30); + scheme.on_primary_container = palette.primary().get(90); + scheme.secondary = palette.secondary().get(80); + scheme.on_secondary = palette.secondary().get(20); + scheme.secondary_container = palette.secondary().get(30); + scheme.on_secondary_container = palette.secondary().get(90); + scheme.tertiary = palette.tertiary().get(80); + scheme.on_tertiary = palette.tertiary().get(20); + scheme.tertiary_container = palette.tertiary().get(30); + scheme.on_tertiary_container = palette.tertiary().get(90); + scheme.error = palette.error().get(80); + scheme.on_error = palette.error().get(20); + scheme.error_container = palette.error().get(30); + scheme.on_error_container = palette.error().get(80); + scheme.background = palette.neutral().get(10); + scheme.on_background = palette.neutral().get(90); + scheme.surface = palette.neutral().get(10); + scheme.on_surface = palette.neutral().get(90); + scheme.surface_variant = palette.neutral_variant().get(30); + scheme.on_surface_variant = palette.neutral_variant().get(80); + scheme.outline = palette.neutral_variant().get(60); + scheme.outline_variant = palette.neutral_variant().get(30); + scheme.shadow = palette.neutral().get(0); + scheme.scrim = palette.neutral().get(0); + scheme.inverse_surface = palette.neutral().get(90); + scheme.inverse_on_surface = palette.neutral().get(20); + scheme.inverse_primary = palette.primary().get(40); + + return scheme; +} + +Scheme MaterialLightColorScheme(Argb color) { + return MaterialLightColorSchemeFromPalette(CorePalette::Of(color)); +} + +Scheme MaterialDarkColorScheme(Argb color) { + return MaterialDarkColorSchemeFromPalette(CorePalette::Of(color)); +} + +Scheme MaterialLightContentColorScheme(Argb color) { + return MaterialLightColorSchemeFromPalette(CorePalette::ContentOf(color)); +} + +Scheme MaterialDarkContentColorScheme(Argb color) { + return MaterialDarkColorSchemeFromPalette(CorePalette::ContentOf(color)); +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme.h b/third_party/material-color/cpp/scheme/scheme.h new file mode 100644 index 0000000..f80bd5e --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme.h @@ -0,0 +1,95 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_H_ +#define CPP_SCHEME_SCHEME_H_ + +#include "cpp/palettes/core.h" +#include "cpp/utils/utils.h" + +// This file is automatically generated. Do not modify it. + +namespace material_color_utilities { + +struct Scheme { + Argb primary = 0; + Argb on_primary = 0; + Argb primary_container = 0; + Argb on_primary_container = 0; + Argb secondary = 0; + Argb on_secondary = 0; + Argb secondary_container = 0; + Argb on_secondary_container = 0; + Argb tertiary = 0; + Argb on_tertiary = 0; + Argb tertiary_container = 0; + Argb on_tertiary_container = 0; + Argb error = 0; + Argb on_error = 0; + Argb error_container = 0; + Argb on_error_container = 0; + Argb background = 0; + Argb on_background = 0; + Argb surface = 0; + Argb on_surface = 0; + Argb surface_variant = 0; + Argb on_surface_variant = 0; + Argb outline = 0; + Argb outline_variant = 0; + Argb shadow = 0; + Argb scrim = 0; + Argb inverse_surface = 0; + Argb inverse_on_surface = 0; + Argb inverse_primary = 0; +}; + +/** + * Returns the light material color scheme based on the given core palette. + */ +Scheme MaterialLightColorSchemeFromPalette(CorePalette palette); + +/** + * Returns the dark material color scheme based on the given core palette. + */ +Scheme MaterialDarkColorSchemeFromPalette(CorePalette palette); + +/** + * Returns the light material color scheme based on the given color, + * in ARGB format. + */ +Scheme MaterialLightColorScheme(Argb color); + +/** + * Returns the dark material color scheme based on the given color, + * in ARGB format. + */ +Scheme MaterialDarkColorScheme(Argb color); + +/** + * Returns the light material content color scheme based on the given color, + * in ARGB format. + */ +Scheme MaterialLightContentColorScheme(Argb color); + +/** + * Returns the dark material content color scheme based on the given color, + * in ARGB format. + */ +Scheme MaterialDarkContentColorScheme(Argb color); + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_content.cc b/third_party/material-color/cpp/scheme/scheme_content.cc new file mode 100644 index 0000000..2f17360 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_content.cc @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_content.h" + +#include "cpp/cam/hct.h" +#include "cpp/dislike/dislike.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" +#include "cpp/temperature/temperature_cache.h" + +namespace material_color_utilities { + +SchemeContent::SchemeContent(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kContent, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + set_source_color_hct.get_chroma()), + /*secondary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + fmax(set_source_color_hct.get_chroma() - 32.0, + set_source_color_hct.get_chroma() * 0.5)), + /*tertiary_palette:*/ + TonalPalette(FixIfDisliked(TemperatureCache(set_source_color_hct) + .GetAnalogousColors(3, 6) + .at(2))), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + set_source_color_hct.get_chroma() / 8.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + set_source_color_hct.get_chroma() / 8.0 + 4.0)) {} + +SchemeContent::SchemeContent(Hct set_source_color_hct, bool set_is_dark) + : SchemeContent::SchemeContent(set_source_color_hct, set_is_dark, 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_content.h b/third_party/material-color/cpp/scheme/scheme_content.h new file mode 100644 index 0000000..9f2b0fd --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_content.h @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_CONTENT_H_ +#define CPP_SCHEME_SCHEME_CONTENT_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeContent : public DynamicScheme { + SchemeContent(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level); + SchemeContent(Hct set_source_color_hct, bool set_is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_CONTENT_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_expressive.cc b/third_party/material-color/cpp/scheme/scheme_expressive.cc new file mode 100644 index 0000000..975d9da --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_expressive.cc @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_expressive.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +const std::vector kHues = {0, 21, 51, 121, 151, 191, 271, 321, 360}; + +const std::vector kSecondaryRotations = {45, 95, 45, 20, 45, + 90, 45, 45, 45}; + +const std::vector kTertiaryRotations = {120, 120, 20, 45, 20, + 15, 20, 120, 120}; + +SchemeExpressive::SchemeExpressive(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kExpressive, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue() + 240.0, 40.0), + /*secondary_palette:*/ + TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues, + kSecondaryRotations), + 24.0), + /*tertiary_palette:*/ + TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues, + kTertiaryRotations), + 32.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue() + 15.0, 8.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue() + 15, 12.0)) {} + +SchemeExpressive::SchemeExpressive(Hct set_source_color_hct, bool set_is_dark) + : SchemeExpressive::SchemeExpressive(set_source_color_hct, set_is_dark, + 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_expressive.h b/third_party/material-color/cpp/scheme/scheme_expressive.h new file mode 100644 index 0000000..3dc3718 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_expressive.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_EXPRESSIVE_H_ +#define CPP_SCHEME_SCHEME_EXPRESSIVE_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeExpressive : public DynamicScheme { + SchemeExpressive(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeExpressive(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_EXPRESSIVE_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_fidelity.cc b/third_party/material-color/cpp/scheme/scheme_fidelity.cc new file mode 100644 index 0000000..fbd7336 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_fidelity.cc @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_fidelity.h" + +#include "cpp/cam/hct.h" +#include "cpp/dislike/dislike.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" +#include "cpp/temperature/temperature_cache.h" + +namespace material_color_utilities { + +SchemeFidelity::SchemeFidelity(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kFidelity, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + set_source_color_hct.get_chroma()), + /*secondary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + fmax(set_source_color_hct.get_chroma() - 32.0, + set_source_color_hct.get_chroma() * 0.5)), + /*tertiary_palette:*/ + TonalPalette(FixIfDisliked( + TemperatureCache(set_source_color_hct).GetComplement())), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + set_source_color_hct.get_chroma() / 8.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), + set_source_color_hct.get_chroma() / 8.0 + 4.0)) {} + +SchemeFidelity::SchemeFidelity(Hct set_source_color_hct, bool set_is_dark) + : SchemeFidelity::SchemeFidelity(set_source_color_hct, set_is_dark, 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_fidelity.h b/third_party/material-color/cpp/scheme/scheme_fidelity.h new file mode 100644 index 0000000..f4068d7 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_fidelity.h @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_FIDELITY_H_ +#define CPP_SCHEME_SCHEME_FIDELITY_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeFidelity : public DynamicScheme { + SchemeFidelity(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level); + SchemeFidelity(Hct set_source_color_hct, bool set_is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_FIDELITY_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_fruit_salad.cc b/third_party/material-color/cpp/scheme/scheme_fruit_salad.cc new file mode 100644 index 0000000..8ddf4f2 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_fruit_salad.cc @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_fruit_salad.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +SchemeFruitSalad::SchemeFruitSalad(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kFruitSalad, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette( + SanitizeDegreesDouble(set_source_color_hct.get_hue() - 50.0), + 48.0), + /*secondary_palette:*/ + TonalPalette( + SanitizeDegreesDouble(set_source_color_hct.get_hue() - 50.0), + 36.0), + /*tertiary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 36.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 10.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 16.0)) {} + +SchemeFruitSalad::SchemeFruitSalad(Hct set_source_color_hct, bool set_is_dark) + : SchemeFruitSalad::SchemeFruitSalad(set_source_color_hct, set_is_dark, + 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_fruit_salad.h b/third_party/material-color/cpp/scheme/scheme_fruit_salad.h new file mode 100644 index 0000000..861cfcf --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_fruit_salad.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_FRUIT_SALAD_H_ +#define CPP_SCHEME_SCHEME_FRUIT_SALAD_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeFruitSalad : public DynamicScheme { + SchemeFruitSalad(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeFruitSalad(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_FRUIT_SALAD_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_monochrome.cc b/third_party/material-color/cpp/scheme/scheme_monochrome.cc new file mode 100644 index 0000000..a801ab0 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_monochrome.cc @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_monochrome.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +SchemeMonochrome::SchemeMonochrome(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kMonochrome, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0), + /*secondary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0), + /*tertiary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0)) {} + +SchemeMonochrome::SchemeMonochrome(Hct set_source_color_hct, bool set_is_dark) + : SchemeMonochrome::SchemeMonochrome(set_source_color_hct, set_is_dark, + 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_monochrome.h b/third_party/material-color/cpp/scheme/scheme_monochrome.h new file mode 100644 index 0000000..42fb8ed --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_monochrome.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_MONOCHROME_H_ +#define CPP_SCHEME_SCHEME_MONOCHROME_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeMonochrome : public DynamicScheme { + SchemeMonochrome(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeMonochrome(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_MONOCHROME_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_monochrome_test.cc b/third_party/material-color/cpp/scheme/scheme_monochrome_test.cc new file mode 100644 index 0000000..77a6544 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_monochrome_test.cc @@ -0,0 +1,95 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_monochrome.h" + +#include "testing/base/public/gunit.h" +#include "cpp/cam/hct.h" +#include "cpp/dynamiccolor/material_dynamic_colors.h" + +namespace material_color_utilities { + +namespace { +TEST(SchemeMonochromeTest, darkTheme_monochromeSpec) { + SchemeMonochrome scheme = SchemeMonochrome(Hct(0xff0000ff), true, 0.0); + EXPECT_NEAR(MaterialDynamicColors::Primary().GetHct(scheme).get_tone(), 100.0, + 1.0); + EXPECT_NEAR(MaterialDynamicColors::OnPrimary().GetHct(scheme).get_tone(), + 10.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::PrimaryContainer().GetHct(scheme).get_tone(), 85.0, + 1.0); + EXPECT_NEAR( + MaterialDynamicColors::OnPrimaryContainer().GetHct(scheme).get_tone(), + 0.0, 1.0); + EXPECT_NEAR(MaterialDynamicColors::Secondary().GetHct(scheme).get_tone(), + 80.0, 1.0); + EXPECT_NEAR(MaterialDynamicColors::OnSecondary().GetHct(scheme).get_tone(), + 10.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::SecondaryContainer().GetHct(scheme).get_tone(), + 30.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::OnSecondaryContainer().GetHct(scheme).get_tone(), + 90.0, 1.0); + EXPECT_NEAR(MaterialDynamicColors::Tertiary().GetHct(scheme).get_tone(), 90.0, + 1.0); + EXPECT_NEAR(MaterialDynamicColors::OnTertiary().GetHct(scheme).get_tone(), + 10.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::TertiaryContainer().GetHct(scheme).get_tone(), + 60.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::OnTertiaryContainer().GetHct(scheme).get_tone(), + 0.0, 1.0); +} + +TEST(SchemeMonochromeTest, lightTheme_monochromeSpec) { + SchemeMonochrome scheme = SchemeMonochrome(Hct(0xff0000ff), false, 0.0); + EXPECT_NEAR(MaterialDynamicColors::Primary().GetHct(scheme).get_tone(), 0.0, + 1.0); + EXPECT_NEAR(MaterialDynamicColors::OnPrimary().GetHct(scheme).get_tone(), + 90.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::PrimaryContainer().GetHct(scheme).get_tone(), 25.0, + 1.0); + EXPECT_NEAR( + MaterialDynamicColors::OnPrimaryContainer().GetHct(scheme).get_tone(), + 100.0, 1.0); + EXPECT_NEAR(MaterialDynamicColors::Secondary().GetHct(scheme).get_tone(), + 40.0, 1.0); + EXPECT_NEAR(MaterialDynamicColors::OnSecondary().GetHct(scheme).get_tone(), + 100.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::SecondaryContainer().GetHct(scheme).get_tone(), + 85.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::OnSecondaryContainer().GetHct(scheme).get_tone(), + 10.0, 1.0); + EXPECT_NEAR(MaterialDynamicColors::Tertiary().GetHct(scheme).get_tone(), 25.0, + 1.0); + EXPECT_NEAR(MaterialDynamicColors::OnTertiary().GetHct(scheme).get_tone(), + 90.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::TertiaryContainer().GetHct(scheme).get_tone(), + 49.0, 1.0); + EXPECT_NEAR( + MaterialDynamicColors::OnTertiaryContainer().GetHct(scheme).get_tone(), + 100.0, 1.0); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_neutral.cc b/third_party/material-color/cpp/scheme/scheme_neutral.cc new file mode 100644 index 0000000..be5541b --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_neutral.cc @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_neutral.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +SchemeNeutral::SchemeNeutral(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kNeutral, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 12.0), + /*secondary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 8.0), + /*tertiary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 16.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 2.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 2.0)) {} + +SchemeNeutral::SchemeNeutral(Hct set_source_color_hct, bool set_is_dark) + : SchemeNeutral::SchemeNeutral(set_source_color_hct, set_is_dark, 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_neutral.h b/third_party/material-color/cpp/scheme/scheme_neutral.h new file mode 100644 index 0000000..56abaf8 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_neutral.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_NEUTRAL_H_ +#define CPP_SCHEME_SCHEME_NEUTRAL_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeNeutral : public DynamicScheme { + SchemeNeutral(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeNeutral(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_NEUTRAL_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_rainbow.cc b/third_party/material-color/cpp/scheme/scheme_rainbow.cc new file mode 100644 index 0000000..7fb5dfb --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_rainbow.cc @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_rainbow.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +SchemeRainbow::SchemeRainbow(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kRainbow, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 48.0), + /*secondary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 16.0), + /*tertiary_palette:*/ + TonalPalette( + SanitizeDegreesDouble(set_source_color_hct.get_hue() + 60.0), + 24.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 0.0)) {} + +SchemeRainbow::SchemeRainbow(Hct set_source_color_hct, bool set_is_dark) + : SchemeRainbow::SchemeRainbow(set_source_color_hct, set_is_dark, 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_rainbow.h b/third_party/material-color/cpp/scheme/scheme_rainbow.h new file mode 100644 index 0000000..2081645 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_rainbow.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_RAINBOW_H_ +#define CPP_SCHEME_SCHEME_RAINBOW_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeRainbow : public DynamicScheme { + SchemeRainbow(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeRainbow(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_RAINBOW_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_test.cc b/third_party/material-color/cpp/scheme/scheme_test.cc new file mode 100644 index 0000000..62bcd41 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_test.cc @@ -0,0 +1,196 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme.h" + +#include "testing/base/public/gunit.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { + +TEST(SchemeTest, SurfaceTones) { + Argb color = 0xff0000ff; + Scheme dark = MaterialDarkColorScheme(color); + EXPECT_NEAR(LstarFromArgb(dark.surface), 10.0, 0.5); + + Scheme light = MaterialLightColorScheme(color); + EXPECT_NEAR(LstarFromArgb(light.surface), 99.0, 0.5); +} + +TEST(SchemeTest, BlueLightScheme) { + Scheme scheme = MaterialLightColorScheme(0xff0000ff); + EXPECT_EQ(HexFromArgb(scheme.primary), "ff343dff"); +} + +TEST(SchemeTest, BlueDarkScheme) { + Scheme scheme = MaterialDarkColorScheme(0xff0000ff); + EXPECT_EQ(HexFromArgb(scheme.primary), "ffbec2ff"); +} + +TEST(SchemeTest, ThirdPartyLightScheme) { + Scheme scheme = MaterialLightColorScheme(0xff6750a4); + EXPECT_EQ(HexFromArgb(scheme.primary), "ff6750a4"); + EXPECT_EQ(HexFromArgb(scheme.secondary), "ff625b71"); + EXPECT_EQ(HexFromArgb(scheme.tertiary), "ff7e5260"); + EXPECT_EQ(HexFromArgb(scheme.surface), "fffffbff"); + EXPECT_EQ(HexFromArgb(scheme.on_surface), "ff1c1b1e"); +} + +TEST(SchemeTest, ThirdPartyDarkScheme) { + Scheme scheme = MaterialDarkColorScheme(0xff6750a4); + EXPECT_EQ(HexFromArgb(scheme.primary), "ffcfbcff"); + EXPECT_EQ(HexFromArgb(scheme.secondary), "ffcbc2db"); + EXPECT_EQ(HexFromArgb(scheme.tertiary), "ffefb8c8"); + EXPECT_EQ(HexFromArgb(scheme.surface), "ff1c1b1e"); + EXPECT_EQ(HexFromArgb(scheme.on_surface), "ffe6e1e6"); +} + +TEST(SchemeTest, LightSchemeFromHighChromaColor) { + Scheme scheme = MaterialLightColorScheme(0xfffa2bec); + EXPECT_EQ(HexFromArgb(scheme.primary), "ffab00a2"); + EXPECT_EQ(HexFromArgb(scheme.on_primary), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.primary_container), "ffffd7f3"); + EXPECT_EQ(HexFromArgb(scheme.on_primary_container), "ff390035"); + EXPECT_EQ(HexFromArgb(scheme.secondary), "ff6e5868"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.secondary_container), "fff8daee"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary_container), "ff271624"); + EXPECT_EQ(HexFromArgb(scheme.tertiary), "ff815343"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.tertiary_container), "ffffdbd0"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary_container), "ff321207"); + EXPECT_EQ(HexFromArgb(scheme.error), "ffba1a1a"); + EXPECT_EQ(HexFromArgb(scheme.on_error), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.error_container), "ffffdad6"); + EXPECT_EQ(HexFromArgb(scheme.on_error_container), "ff410002"); + EXPECT_EQ(HexFromArgb(scheme.background), "fffffbff"); + EXPECT_EQ(HexFromArgb(scheme.on_background), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.surface), "fffffbff"); + EXPECT_EQ(HexFromArgb(scheme.on_surface), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.surface_variant), "ffeedee7"); + EXPECT_EQ(HexFromArgb(scheme.on_surface_variant), "ff4e444b"); + EXPECT_EQ(HexFromArgb(scheme.outline), "ff80747b"); + EXPECT_EQ(HexFromArgb(scheme.outline_variant), "ffd2c2cb"); + EXPECT_EQ(HexFromArgb(scheme.shadow), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.scrim), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.inverse_surface), "ff342f32"); + EXPECT_EQ(HexFromArgb(scheme.inverse_on_surface), "fff8eef2"); + EXPECT_EQ(HexFromArgb(scheme.inverse_primary), "ffffabee"); +} + +TEST(SchemeTest, DarkSchemeFromHighChromaColor) { + Scheme scheme = MaterialDarkColorScheme(0xfffa2bec); + EXPECT_EQ(HexFromArgb(scheme.primary), "ffffabee"); + EXPECT_EQ(HexFromArgb(scheme.on_primary), "ff5c0057"); + EXPECT_EQ(HexFromArgb(scheme.primary_container), "ff83007b"); + EXPECT_EQ(HexFromArgb(scheme.on_primary_container), "ffffd7f3"); + EXPECT_EQ(HexFromArgb(scheme.secondary), "ffdbbed1"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary), "ff3e2a39"); + EXPECT_EQ(HexFromArgb(scheme.secondary_container), "ff554050"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary_container), "fff8daee"); + EXPECT_EQ(HexFromArgb(scheme.tertiary), "fff5b9a5"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary), "ff4c2619"); + EXPECT_EQ(HexFromArgb(scheme.tertiary_container), "ff663c2d"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary_container), "ffffdbd0"); + EXPECT_EQ(HexFromArgb(scheme.error), "ffffb4ab"); + EXPECT_EQ(HexFromArgb(scheme.on_error), "ff690005"); + EXPECT_EQ(HexFromArgb(scheme.error_container), "ff93000a"); + EXPECT_EQ(HexFromArgb(scheme.on_error_container), "ffffb4ab"); + EXPECT_EQ(HexFromArgb(scheme.background), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.on_background), "ffeae0e4"); + EXPECT_EQ(HexFromArgb(scheme.surface), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.on_surface), "ffeae0e4"); + EXPECT_EQ(HexFromArgb(scheme.surface_variant), "ff4e444b"); + EXPECT_EQ(HexFromArgb(scheme.on_surface_variant), "ffd2c2cb"); + EXPECT_EQ(HexFromArgb(scheme.outline), "ff9a8d95"); + EXPECT_EQ(HexFromArgb(scheme.outline_variant), "ff4e444b"); + EXPECT_EQ(HexFromArgb(scheme.shadow), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.scrim), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.inverse_surface), "ffeae0e4"); + EXPECT_EQ(HexFromArgb(scheme.inverse_on_surface), "ff342f32"); + EXPECT_EQ(HexFromArgb(scheme.inverse_primary), "ffab00a2"); +} + +TEST(SchemeTest, LightContentSchemeFromHighChromaColor) { + Scheme scheme = MaterialLightContentColorScheme(0xfffa2bec); + EXPECT_EQ(HexFromArgb(scheme.primary), "ffab00a2"); + EXPECT_EQ(HexFromArgb(scheme.on_primary), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.primary_container), "ffffd7f3"); + EXPECT_EQ(HexFromArgb(scheme.on_primary_container), "ff390035"); + EXPECT_EQ(HexFromArgb(scheme.secondary), "ff7f4e75"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.secondary_container), "ffffd7f3"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary_container), "ff330b2f"); + EXPECT_EQ(HexFromArgb(scheme.tertiary), "ff9c4323"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.tertiary_container), "ffffdbd0"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary_container), "ff390c00"); + EXPECT_EQ(HexFromArgb(scheme.error), "ffba1a1a"); + EXPECT_EQ(HexFromArgb(scheme.on_error), "ffffffff"); + EXPECT_EQ(HexFromArgb(scheme.error_container), "ffffdad6"); + EXPECT_EQ(HexFromArgb(scheme.on_error_container), "ff410002"); + EXPECT_EQ(HexFromArgb(scheme.background), "fffffbff"); + EXPECT_EQ(HexFromArgb(scheme.on_background), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.surface), "fffffbff"); + EXPECT_EQ(HexFromArgb(scheme.on_surface), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.surface_variant), "ffeedee7"); + EXPECT_EQ(HexFromArgb(scheme.on_surface_variant), "ff4e444b"); + EXPECT_EQ(HexFromArgb(scheme.outline), "ff80747b"); + EXPECT_EQ(HexFromArgb(scheme.outline_variant), "ffd2c2cb"); + EXPECT_EQ(HexFromArgb(scheme.shadow), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.scrim), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.inverse_surface), "ff342f32"); + EXPECT_EQ(HexFromArgb(scheme.inverse_on_surface), "fff8eef2"); + EXPECT_EQ(HexFromArgb(scheme.inverse_primary), "ffffabee"); +} + +TEST(SchemeTest, DarkContentSchemeFromHighChromaColor) { + Scheme scheme = MaterialDarkContentColorScheme(0xfffa2bec); + EXPECT_EQ(HexFromArgb(scheme.primary), "ffffabee"); + EXPECT_EQ(HexFromArgb(scheme.on_primary), "ff5c0057"); + EXPECT_EQ(HexFromArgb(scheme.primary_container), "ff83007b"); + EXPECT_EQ(HexFromArgb(scheme.on_primary_container), "ffffd7f3"); + EXPECT_EQ(HexFromArgb(scheme.secondary), "fff0b4e1"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary), "ff4b2145"); + EXPECT_EQ(HexFromArgb(scheme.secondary_container), "ff64375c"); + EXPECT_EQ(HexFromArgb(scheme.on_secondary_container), "ffffd7f3"); + EXPECT_EQ(HexFromArgb(scheme.tertiary), "ffffb59c"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary), "ff5c1900"); + EXPECT_EQ(HexFromArgb(scheme.tertiary_container), "ff7d2c0d"); + EXPECT_EQ(HexFromArgb(scheme.on_tertiary_container), "ffffdbd0"); + EXPECT_EQ(HexFromArgb(scheme.error), "ffffb4ab"); + EXPECT_EQ(HexFromArgb(scheme.on_error), "ff690005"); + EXPECT_EQ(HexFromArgb(scheme.error_container), "ff93000a"); + EXPECT_EQ(HexFromArgb(scheme.on_error_container), "ffffb4ab"); + EXPECT_EQ(HexFromArgb(scheme.background), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.on_background), "ffeae0e4"); + EXPECT_EQ(HexFromArgb(scheme.surface), "ff1f1a1d"); + EXPECT_EQ(HexFromArgb(scheme.on_surface), "ffeae0e4"); + EXPECT_EQ(HexFromArgb(scheme.surface_variant), "ff4e444b"); + EXPECT_EQ(HexFromArgb(scheme.on_surface_variant), "ffd2c2cb"); + EXPECT_EQ(HexFromArgb(scheme.outline), "ff9a8d95"); + EXPECT_EQ(HexFromArgb(scheme.outline_variant), "ff4e444b"); + EXPECT_EQ(HexFromArgb(scheme.shadow), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.scrim), "ff000000"); + EXPECT_EQ(HexFromArgb(scheme.inverse_surface), "ffeae0e4"); + EXPECT_EQ(HexFromArgb(scheme.inverse_on_surface), "ff342f32"); + EXPECT_EQ(HexFromArgb(scheme.inverse_primary), "ffab00a2"); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_tonal_spot.cc b/third_party/material-color/cpp/scheme/scheme_tonal_spot.cc new file mode 100644 index 0000000..5ebd17e --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_tonal_spot.cc @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_tonal_spot.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +SchemeTonalSpot::SchemeTonalSpot(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kTonalSpot, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 36.0), + /*secondary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 16.0), + /*tertiary_palette:*/ + TonalPalette( + SanitizeDegreesDouble(set_source_color_hct.get_hue() + 60), 24.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 6.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 8.0)) {} + +SchemeTonalSpot::SchemeTonalSpot(Hct set_source_color_hct, bool set_is_dark) + : SchemeTonalSpot::SchemeTonalSpot(set_source_color_hct, set_is_dark, 0.0) { +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_tonal_spot.h b/third_party/material-color/cpp/scheme/scheme_tonal_spot.h new file mode 100644 index 0000000..0da4957 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_tonal_spot.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_TONAL_SPOT_H_ +#define CPP_SCHEME_SCHEME_TONAL_SPOT_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeTonalSpot : public DynamicScheme { + SchemeTonalSpot(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeTonalSpot(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_TONAL_SPOT_H_ diff --git a/third_party/material-color/cpp/scheme/scheme_vibrant.cc b/third_party/material-color/cpp/scheme/scheme_vibrant.cc new file mode 100644 index 0000000..211b289 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_vibrant.cc @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/scheme/scheme_vibrant.h" + +#include "cpp/cam/hct.h" +#include "cpp/palettes/tones.h" +#include "cpp/scheme/dynamic_scheme.h" +#include "cpp/scheme/variant.h" + +namespace material_color_utilities { + +const std::vector kHues = {0, 41, 61, 101, 131, 181, 251, 301, 360}; + +const std::vector kSecondaryRotations = {18, 15, 10, 12, 15, + 18, 15, 12, 12}; + +const std::vector kTertiaryRotations = {35, 30, 20, 25, 30, + 35, 30, 25, 25}; + +SchemeVibrant::SchemeVibrant(Hct set_source_color_hct, bool set_is_dark, + double set_contrast_level) + : DynamicScheme( + /*source_color_argb:*/ set_source_color_hct.ToInt(), + /*variant:*/ Variant::kVibrant, + /*contrast_level:*/ set_contrast_level, + /*is_dark:*/ set_is_dark, + /*primary_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 200.0), + /*secondary_palette:*/ + TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues, + kSecondaryRotations), + 24.0), + /*tertiary_palette:*/ + TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues, + kTertiaryRotations), + 32.0), + /*neutral_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 10.0), + /*neutral_variant_palette:*/ + TonalPalette(set_source_color_hct.get_hue(), 12.0)) {} + +SchemeVibrant::SchemeVibrant(Hct set_source_color_hct, bool set_is_dark) + : SchemeVibrant::SchemeVibrant(set_source_color_hct, set_is_dark, 0.0) {} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/scheme/scheme_vibrant.h b/third_party/material-color/cpp/scheme/scheme_vibrant.h new file mode 100644 index 0000000..5d89cc2 --- /dev/null +++ b/third_party/material-color/cpp/scheme/scheme_vibrant.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_SCHEME_VARIANT_H_ +#define CPP_SCHEME_SCHEME_VARIANT_H_ + +#include "cpp/cam/hct.h" +#include "cpp/scheme/dynamic_scheme.h" + +namespace material_color_utilities { + +struct SchemeVibrant : public DynamicScheme { + SchemeVibrant(Hct source_color_hct, bool is_dark, double contrast_level); + SchemeVibrant(Hct source_color_hct, bool is_dark); +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_SCHEME_VARIANT_H_ diff --git a/third_party/material-color/cpp/scheme/variant.h b/third_party/material-color/cpp/scheme/variant.h new file mode 100644 index 0000000..aa57b33 --- /dev/null +++ b/third_party/material-color/cpp/scheme/variant.h @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCHEME_VARIANT_H_ +#define CPP_SCHEME_VARIANT_H_ + +namespace material_color_utilities { + +enum class Variant { + kMonochrome, + kNeutral, + kTonalSpot, + kVibrant, + kExpressive, + kFidelity, + kContent, + kRainbow, + kFruitSalad, +}; + +} // namespace material_color_utilities + +#endif // CPP_SCHEME_VARIANT_H_ diff --git a/third_party/material-color/cpp/score/score.cc b/third_party/material-color/cpp/score/score.cc new file mode 100644 index 0000000..c2c0278 --- /dev/null +++ b/third_party/material-color/cpp/score/score.cc @@ -0,0 +1,124 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/score/score.h" + +#include +#include +#include +#include +#include +#include + +#include "cpp/cam/hct.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +constexpr double kTargetChroma = 48.0; // A1 Chroma +constexpr double kWeightProportion = 0.7; +constexpr double kWeightChromaAbove = 0.3; +constexpr double kWeightChromaBelow = 0.1; +constexpr double kCutoffChroma = 5.0; +constexpr double kCutoffExcitedProportion = 0.01; + +bool CompareScoredHCT(const std::pair& a, + const std::pair& b) { + return a.second > b.second; +} + +std::vector RankedSuggestions( + const std::map& argb_to_population, + const ScoreOptions& options) { + // Get the HCT color for each Argb value, while finding the per hue count and + // total count. + std::vector colors_hct; + std::vector hue_population(360, 0); + double population_sum = 0; + for (const auto& [argb, population] : argb_to_population) { + Hct hct(argb); + colors_hct.push_back(hct); + int hue = floor(hct.get_hue()); + hue_population[hue] += population; + population_sum += population; + } + + // Hues with more usage in neighboring 30 degree slice get a larger number. + std::vector hue_excited_proportions(360, 0.0); + for (int hue = 0; hue < 360; hue++) { + double proportion = hue_population[hue] / population_sum; + for (int i = hue - 14; i < hue + 16; i++) { + int neighbor_hue = SanitizeDegreesInt(i); + hue_excited_proportions[neighbor_hue] += proportion; + } + } + + // Scores each HCT color based on usage and chroma, while optionally + // filtering out values that do not have enough chroma or usage. + std::vector> scored_hcts; + for (Hct hct : colors_hct) { + int hue = SanitizeDegreesInt(round(hct.get_hue())); + double proportion = hue_excited_proportions[hue]; + if (options.filter && (hct.get_chroma() < kCutoffChroma || + proportion <= kCutoffExcitedProportion)) { + continue; + } + + double proportion_score = proportion * 100.0 * kWeightProportion; + double chroma_weight = hct.get_chroma() < kTargetChroma + ? kWeightChromaBelow + : kWeightChromaAbove; + double chroma_score = (hct.get_chroma() - kTargetChroma) * chroma_weight; + double score = proportion_score + chroma_score; + scored_hcts.push_back({hct, score}); + } + // Sorted so that colors with higher scores come first. + sort(scored_hcts.begin(), scored_hcts.end(), CompareScoredHCT); + + // Iterates through potential hue differences in degrees in order to select + // the colors with the largest distribution of hues possible. Starting at + // 90 degrees(maximum difference for 4 colors) then decreasing down to a + // 15 degree minimum. + std::vector chosen_colors; + for (int difference_degrees = 90; difference_degrees >= 15; + difference_degrees--) { + chosen_colors.clear(); + for (auto entry : scored_hcts) { + Hct hct = entry.first; + auto duplicate_hue = std::find_if( + chosen_colors.begin(), chosen_colors.end(), + [&hct, difference_degrees](Hct chosen_hct) { + return DiffDegrees(hct.get_hue(), chosen_hct.get_hue()) < + difference_degrees; + }); + if (duplicate_hue == chosen_colors.end()) { + chosen_colors.push_back(hct); + if (chosen_colors.size() >= options.desired) break; + } + } + if (chosen_colors.size() >= options.desired) break; + } + std::vector colors; + if (chosen_colors.empty()) { + colors.push_back(options.fallback_color_argb); + } + for (auto chosen_hct : chosen_colors) { + colors.push_back(chosen_hct.ToInt()); + } + return colors; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/score/score.h b/third_party/material-color/cpp/score/score.h new file mode 100644 index 0000000..5bd2b82 --- /dev/null +++ b/third_party/material-color/cpp/score/score.h @@ -0,0 +1,59 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_SCORE_SCORE_H_ +#define CPP_SCORE_SCORE_H_ + +#include +#include +#include + +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +/** + * Default options for ranking colors based on usage counts. + * `desired`: is the max count of the colors returned. + * `fallback_color_argb`: Is the default color that should be used if no + * other colors are suitable. + * `filter`: controls if the resulting colors should be filtered to not include + * hues that are not used often enough, and colors that are effectively + * grayscale. + */ +struct ScoreOptions { + size_t desired = 4; // 4 colors matches the Android wallpaper picker. + int fallback_color_argb = 0xff4285f4; // Google Blue. + bool filter = true; // Avoid unsuitable colors. +}; + +/** + * Given a map with keys of colors and values of how often the color appears, + * rank the colors based on suitability for being used for a UI theme. + * + * The list returned is of length <= [desired]. The recommended color is the + * first item, the least suitable is the last. There will always be at least + * one color returned. If all the input colors were not suitable for a theme, + * a default fallback color will be provided, Google Blue, or supplied fallback + * color. The default number of colors returned is 4, simply because that's the + * # of colors display in Android 12's wallpaper picker. + */ +std::vector RankedSuggestions( + const std::map& argb_to_population, + const ScoreOptions& options = {}); +} // namespace material_color_utilities + +#endif // CPP_SCORE_SCORE_H_ diff --git a/third_party/material-color/cpp/score/score_test.cc b/third_party/material-color/cpp/score/score_test.cc new file mode 100644 index 0000000..8f8100d --- /dev/null +++ b/third_party/material-color/cpp/score/score_test.cc @@ -0,0 +1,257 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/score/score.h" + +#include +#include +#include + +#include "testing/base/public/gunit.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +namespace { + +TEST(ScoreTest, PrioritizesChroma) { + std::map argb_to_population = { + {0xff000000, 1}, {0xffffffff, 1}, {0xff0000ff, 1}}; + + std::vector ranked = + RankedSuggestions(argb_to_population, {.desired = 4}); + + EXPECT_EQ(ranked.size(), 1u); + EXPECT_EQ(ranked[0], 0xff0000ff); +} + +TEST(ScoreTest, PrioritizesChromaWhenProportionsEqual) { + std::map argb_to_population = { + {0xffff0000, 1}, {0xff00ff00, 1}, {0xff0000ff, 1}}; + + std::vector ranked = + RankedSuggestions(argb_to_population, {.desired = 4}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xffff0000); + EXPECT_EQ(ranked[1], 0xff00ff00); + EXPECT_EQ(ranked[2], 0xff0000ff); +} + +TEST(ScoreTest, GeneratesGblueWhenNoColorsAvailable) { + std::map argb_to_population = {{0xff000000, 1}}; + + std::vector ranked = + RankedSuggestions(argb_to_population, {.desired = 4}); + + EXPECT_EQ(ranked.size(), 1u); + EXPECT_EQ(ranked[0], 0xff4285f4); +} + +TEST(ScoreTest, DedupesNearbyHues) { + std::map argb_to_population = { + {0xff008772, 1}, // H 180 C 42 T 50 + {0xff318477, 1} // H 184 C 35 T 50 + }; + + std::vector ranked = + RankedSuggestions(argb_to_population, {.desired = 4}); + + EXPECT_EQ(ranked.size(), 1u); + EXPECT_EQ(ranked[0], 0xff008772); +} + +TEST(ScoreTest, MaximizesHueDistance) { + std::map argb_to_population = { + {0xff008772, 1}, // H 180 C 42 T 50 + {0xff008587, 1}, // H 198 C 50 T 50 + {0xff007ebc, 1} // H 245 C 50 T 50 + }; + + std::vector ranked = + RankedSuggestions(argb_to_population, {.desired = 2}); + + EXPECT_EQ(ranked.size(), 2u); + EXPECT_EQ(ranked[0], 0xff007ebc); + EXPECT_EQ(ranked[1], 0xff008772); +} + +TEST(ScoreTest, GeneratedScenarioOne) { + std::map argb_to_population = { + {0xff7ea16d, 67}, + {0xffd8ccae, 67}, + {0xff835c0d, 49}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 3, .fallback_color_argb = (int)0xff8d3819, .filter = false}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xff7ea16d); + EXPECT_EQ(ranked[1], 0xffd8ccae); + EXPECT_EQ(ranked[2], 0xff835c0d); +} + +TEST(ScoreTest, GeneratedScenarioTwo) { + std::map argb_to_population = { + {0xffd33881, 14}, + {0xff3205cc, 77}, + {0xff0b48cf, 36}, + {0xffa08f5d, 81}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 4, .fallback_color_argb = (int)0xff7d772b, .filter = true}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xff3205cc); + EXPECT_EQ(ranked[1], 0xffa08f5d); + EXPECT_EQ(ranked[2], 0xffd33881); +} + +TEST(ScoreTest, GeneratedScenarioThree) { + std::map argb_to_population = { + {0xffbe94a6, 23}, + {0xffc33fd7, 42}, + {0xff899f36, 90}, + {0xff94c574, 82}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 3, .fallback_color_argb = (int)0xffaa79a4, .filter = true}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xff94c574); + EXPECT_EQ(ranked[1], 0xffc33fd7); + EXPECT_EQ(ranked[2], 0xffbe94a6); +} + +TEST(ScoreTest, GeneratedScenarioFour) { + std::map argb_to_population = { + {0xffdf241c, 85}, {0xff685859, 44}, {0xffd06d5f, 34}, + {0xff561c54, 27}, {0xff713090, 88}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 5, .fallback_color_argb = (int)0xff58c19c, .filter = false}); + + EXPECT_EQ(ranked.size(), 2u); + EXPECT_EQ(ranked[0], 0xffdf241c); + EXPECT_EQ(ranked[1], 0xff561c54); +} + +TEST(ScoreTest, GeneratedScenarioFive) { + std::map argb_to_population = { + {0xffbe66f8, 41}, {0xff4bbda9, 88}, {0xff80f6f9, 44}, + {0xffab8017, 43}, {0xffe89307, 65}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 3, .fallback_color_argb = (int)0xff916691, .filter = false}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xffab8017); + EXPECT_EQ(ranked[1], 0xff4bbda9); + EXPECT_EQ(ranked[2], 0xffbe66f8); +} + +TEST(ScoreTest, GeneratedScenarioSix) { + std::map argb_to_population = { + {0xff18ea8f, 93}, {0xff327593, 18}, {0xff066a18, 53}, + {0xfffa8a23, 74}, {0xff04ca1f, 62}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 2, .fallback_color_argb = (int)0xff4c377a, .filter = false}); + + EXPECT_EQ(ranked.size(), 2u); + EXPECT_EQ(ranked[0], 0xff18ea8f); + EXPECT_EQ(ranked[1], 0xfffa8a23); +} + +TEST(ScoreTest, GeneratedScenarioSeven) { + std::map argb_to_population = { + {0xff2e05ed, 23}, {0xff153e55, 90}, {0xff9ab220, 23}, + {0xff153379, 66}, {0xff68bcc3, 81}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 2, .fallback_color_argb = (int)0xfff588dc, .filter = true}); + + EXPECT_EQ(ranked.size(), 2u); + EXPECT_EQ(ranked[0], 0xff2e05ed); + EXPECT_EQ(ranked[1], 0xff9ab220); +} + +TEST(ScoreTest, GeneratedScenarioEight) { + std::map argb_to_population = { + {0xff816ec5, 24}, + {0xff6dcb94, 19}, + {0xff3cae91, 98}, + {0xff5b542f, 25}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 1, .fallback_color_argb = (int)0xff84b0fd, .filter = false}); + + EXPECT_EQ(ranked.size(), 1u); + EXPECT_EQ(ranked[0], 0xff3cae91); +} + +TEST(ScoreTest, GeneratedScenarioNine) { + std::map argb_to_population = { + {0xff206f86, 52}, {0xff4a620d, 96}, {0xfff51401, 85}, + {0xff2b8ebf, 3}, {0xff277766, 59}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 3, .fallback_color_argb = (int)0xff02b415, .filter = true}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xfff51401); + EXPECT_EQ(ranked[1], 0xff4a620d); + EXPECT_EQ(ranked[2], 0xff2b8ebf); +} + +TEST(ScoreTest, GeneratedScenarioTen) { + std::map argb_to_population = { + {0xff8b1d99, 54}, + {0xff27effe, 43}, + {0xff6f558d, 2}, + {0xff77fdf2, 78}, + }; + + std::vector ranked = RankedSuggestions( + argb_to_population, + {.desired = 4, .fallback_color_argb = (int)0xff5e7a10, .filter = true}); + + EXPECT_EQ(ranked.size(), 3u); + EXPECT_EQ(ranked[0], 0xff27effe); + EXPECT_EQ(ranked[1], 0xff8b1d99); + EXPECT_EQ(ranked[2], 0xff6f558d); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/temperature/temperature_cache.cc b/third_party/material-color/cpp/temperature/temperature_cache.cc new file mode 100644 index 0000000..c90b440 --- /dev/null +++ b/third_party/material-color/cpp/temperature/temperature_cache.cc @@ -0,0 +1,248 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/temperature/temperature_cache.h" + +#include +#include + +#include "cpp/cam/hct.h" +#include "cpp/quantize/lab.h" +#include "cpp/utils/utils.h" + +namespace material_color_utilities { + +TemperatureCache::TemperatureCache(Hct input) : input_(input) {} + +Hct TemperatureCache::GetComplement() { + if (precomputed_complement_.has_value()) { + return precomputed_complement_.value(); + } + + double coldest_hue = GetColdest().get_hue(); + double coldest_temp = GetTempsByHct().at(GetColdest()); + + double warmest_hue = GetWarmest().get_hue(); + double warmest_temp = GetTempsByHct().at(GetWarmest()); + double range = warmest_temp - coldest_temp; + bool start_hue_is_coldest_to_warmest = + IsBetween(input_.get_hue(), coldest_hue, warmest_hue); + double start_hue = + start_hue_is_coldest_to_warmest ? warmest_hue : coldest_hue; + double end_hue = start_hue_is_coldest_to_warmest ? coldest_hue : warmest_hue; + double direction_of_rotation = 1.0; + double smallest_error = 1000.0; + Hct answer = GetHctsByHue().at((int)round(input_.get_hue())); + + double complement_relative_temp = (1.0 - GetRelativeTemperature(input_)); + // Find the color in the other section, closest to the inverse percentile + // of the input color. This is the complement. + for (double hue_addend = 0.0; hue_addend <= 360.0; hue_addend += 1.0) { + double hue = + SanitizeDegreesDouble(start_hue + direction_of_rotation * hue_addend); + if (!IsBetween(hue, start_hue, end_hue)) { + continue; + } + Hct possible_answer = GetHctsByHue().at((int)round(hue)); + double relative_temp = + (GetTempsByHct().at(possible_answer) - coldest_temp) / range; + double error = abs(complement_relative_temp - relative_temp); + if (error < smallest_error) { + smallest_error = error; + answer = possible_answer; + } + } + precomputed_complement_ = answer; + return precomputed_complement_.value(); +} + +std::vector TemperatureCache::GetAnalogousColors() { + return GetAnalogousColors(5, 12); +} + +std::vector TemperatureCache::GetAnalogousColors(int count, + int divisions) { + // The starting hue is the hue of the input color. + int start_hue = (int)round(input_.get_hue()); + Hct start_hct = GetHctsByHue().at(start_hue); + double last_temp = GetRelativeTemperature(start_hct); + + std::vector all_colors; + all_colors.push_back(start_hct); + + double absolute_total_temp_delta = 0.0; + for (int i = 0; i < 360; i++) { + int hue = SanitizeDegreesInt(start_hue + i); + Hct hct = GetHctsByHue().at(hue); + double temp = GetRelativeTemperature(hct); + double temp_delta = abs(temp - last_temp); + last_temp = temp; + absolute_total_temp_delta += temp_delta; + } + + int hue_addend = 1; + double temp_step = absolute_total_temp_delta / (double)divisions; + double total_temp_delta = 0.0; + last_temp = GetRelativeTemperature(start_hct); + while (all_colors.size() < static_cast(divisions)) { + int hue = SanitizeDegreesInt(start_hue + hue_addend); + Hct hct = GetHctsByHue().at(hue); + double temp = GetRelativeTemperature(hct); + double temp_delta = abs(temp - last_temp); + total_temp_delta += temp_delta; + + double desired_total_temp_delta_for_index = (all_colors.size() * temp_step); + bool index_satisfied = + total_temp_delta >= desired_total_temp_delta_for_index; + int index_addend = 1; + // Keep adding this hue to the answers until its temperature is + // insufficient. This ensures consistent behavior when there aren't + // `divisions` discrete steps between 0 and 360 in hue with `temp_step` + // delta in temperature between them. + // + // For example, white and black have no analogues: there are no other + // colors at T100/T0. Therefore, they should just be added to the array + // as answers. + while (index_satisfied && + all_colors.size() < static_cast(divisions)) { + all_colors.push_back(hct); + desired_total_temp_delta_for_index = + ((all_colors.size() + index_addend) * temp_step); + index_satisfied = total_temp_delta >= desired_total_temp_delta_for_index; + index_addend++; + } + last_temp = temp; + hue_addend++; + + if (hue_addend > 360) { + while (all_colors.size() < static_cast(divisions)) { + all_colors.push_back(hct); + } + break; + } + } + + std::vector answers; + answers.push_back(input_); + + int ccw_count = (int)floor(((double)count - 1.0) / 2.0); + for (int i = 1; i < (ccw_count + 1); i++) { + int index = 0 - i; + while (index < 0) { + index = all_colors.size() + index; + } + if (static_cast(index) >= all_colors.size()) { + index = index % all_colors.size(); + } + answers.insert(answers.begin(), all_colors.at(index)); + } + + int cw_count = count - ccw_count - 1; + for (int i = 1; i < (cw_count + 1); i++) { + size_t index = i; + while (index < 0) { + index = all_colors.size() + index; + } + if (index >= all_colors.size()) { + index = index % all_colors.size(); + } + answers.push_back(all_colors.at(index)); + } + + return answers; +} + +double TemperatureCache::GetRelativeTemperature(Hct hct) { + double range = + GetTempsByHct().at(GetWarmest()) - GetTempsByHct().at(GetColdest()); + double difference_from_coldest = + GetTempsByHct().at(hct) - GetTempsByHct().at(GetColdest()); + // Handle when there's no difference in temperature between warmest and + // coldest: for example, at T100, only one color is available, white. + if (range == 0.) { + return 0.5; + } + return difference_from_coldest / range; +} + +double TemperatureCache::RawTemperature(Hct color) { + Lab lab = LabFromInt(color.ToInt()); + double hue = SanitizeDegreesDouble(atan2(lab.b, lab.a) * 180.0 / kPi); + double chroma = hypot(lab.a, lab.b); + return -0.5 + 0.02 * pow(chroma, 1.07) * + cos(SanitizeDegreesDouble(hue - 50.) * kPi / 180); +} + +Hct TemperatureCache::GetColdest() { return GetHctsByTemp().at(0); } + +std::vector TemperatureCache::GetHctsByHue() { + if (precomputed_hcts_by_hue_.has_value()) { + return precomputed_hcts_by_hue_.value(); + } + std::vector hcts; + for (double hue = 0.; hue <= 360.; hue += 1.) { + Hct color_at_hue(hue, input_.get_chroma(), input_.get_tone()); + hcts.push_back(color_at_hue); + } + precomputed_hcts_by_hue_ = hcts; + return precomputed_hcts_by_hue_.value(); +} + +std::vector TemperatureCache::GetHctsByTemp() { + if (precomputed_hcts_by_temp_.has_value()) { + return precomputed_hcts_by_temp_.value(); + } + + std::vector hcts(GetHctsByHue()); + hcts.push_back(input_); + std::map temps_by_hct(GetTempsByHct()); + sort(hcts.begin(), hcts.end(), + [temps_by_hct](const Hct a, const Hct b) -> bool { + return temps_by_hct.at(a) < temps_by_hct.at(b); + }); + precomputed_hcts_by_temp_ = hcts; + return precomputed_hcts_by_temp_.value(); +} + +std::map TemperatureCache::GetTempsByHct() { + if (precomputed_temps_by_hct_.has_value()) { + return precomputed_temps_by_hct_.value(); + } + + std::vector all_hcts(GetHctsByHue()); + all_hcts.push_back(input_); + + std::map temperatures_by_hct; + for (Hct hct : all_hcts) { + temperatures_by_hct[hct] = RawTemperature(hct); + } + + precomputed_temps_by_hct_ = temperatures_by_hct; + return precomputed_temps_by_hct_.value(); +} + +Hct TemperatureCache::GetWarmest() { + return GetHctsByTemp().at(GetHctsByTemp().size() - 1); +} + +bool TemperatureCache::IsBetween(double angle, double a, double b) { + if (a < b) { + return a <= angle && angle <= b; + } + return a <= angle || angle <= b; +} + +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/temperature/temperature_cache.h b/third_party/material-color/cpp/temperature/temperature_cache.h new file mode 100644 index 0000000..17e7c17 --- /dev/null +++ b/third_party/material-color/cpp/temperature/temperature_cache.h @@ -0,0 +1,139 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_TEMPERATURE_TEMPERATURE_CACHE_H_ +#define CPP_TEMPERATURE_TEMPERATURE_CACHE_H_ + +#include +#include + +#include "cpp/cam/hct.h" + +namespace material_color_utilities { + +/** + * Design utilities using color temperature theory. + * + *

Analogous colors, complementary color, and cache to efficiently, lazily, + * generate data for calculations when needed. + */ +class TemperatureCache { + public: + /** + * Create a cache that allows calculation of ex. complementary and analogous + * colors. + * + * @param input Color to find complement/analogous colors of. Any colors will + * have the same tone, and chroma as the input color, modulo any restrictions + * due to the other hues having lower limits on chroma. + */ + explicit TemperatureCache(Hct input); + + /** + * A color that complements the input color aesthetically. + * + *

In art, this is usually described as being across the color wheel. + * History of this shows intent as a color that is just as cool-warm as the + * input color is warm-cool. + */ + Hct GetComplement(); + + /** + * 5 colors that pair well with the input color. + * + *

The colors are equidistant in temperature and adjacent in hue. + */ + std::vector GetAnalogousColors(); + + /** + * A set of colors with differing hues, equidistant in temperature. + * + *

In art, this is usually described as a set of 5 colors on a color wheel + * divided into 12 sections. This method allows provision of either of those + * values. + * + *

Behavior is undefined when count or divisions is 0. When divisions < + * count, colors repeat. + * + * @param count The number of colors to return, includes the input color. + * @param divisions The number of divisions on the color wheel. + */ + std::vector GetAnalogousColors(int count, int divisions); + + /** + * Temperature relative to all colors with the same chroma and tone. + * + * @param hct HCT to find the relative temperature of. + * @return Value on a scale from 0 to 1. + */ + double GetRelativeTemperature(Hct hct); + + /** + * Value representing cool-warm factor of a color. Values below 0 are + * considered cool, above, warm. + * + *

Color science has researched emotion and harmony, which art uses to + * select colors. Warm-cool is the foundation of analogous and complementary + * colors. See: - Li-Chen Ou's Chapter 19 in Handbook of Color Psychology + * (2015). - Josef Albers' Interaction of Color chapters 19 and 21. + * + *

Implementation of Ou, Woodcock and Wright's algorithm, which uses + * Lab/LCH color space. Return value has these properties:
+ * - Values below 0 are cool, above 0 are warm.
+ * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma + * 130.
+ * - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130. + */ + static double RawTemperature(Hct color); + + private: + Hct input_; + + std::optional precomputed_complement_; + std::optional> precomputed_hcts_by_temp_; + std::optional> precomputed_hcts_by_hue_; + std::optional> precomputed_temps_by_hct_; + + /** Coldest color with same chroma and tone as input. */ + Hct GetColdest(); + + /** Warmest color with same chroma and tone as input. */ + Hct GetWarmest(); + + /** Determines if an angle is between two other angles, rotating clockwise. */ + static bool IsBetween(double angle, double a, double b); + + /** + * HCTs for all colors with the same chroma/tone as the input. + * + *

Sorted by hue, ex. index 0 is hue 0. + */ + std::vector GetHctsByHue(); + + /** + * HCTs for all colors with the same chroma/tone as the input. + * + *

Sorted from coldest first to warmest last. + */ + std::vector GetHctsByTemp(); + + /** Keys of HCTs in GetHctsByTemp, values of raw temperature. */ + std::map GetTempsByHct(); +}; + +} // namespace material_color_utilities + +#endif // CPP_TEMPERATURE_TEMPERATURE_CACHE_H_ diff --git a/third_party/material-color/cpp/temperature/temperature_cache_test.cc b/third_party/material-color/cpp/temperature/temperature_cache_test.cc new file mode 100644 index 0000000..f4a6f83 --- /dev/null +++ b/third_party/material-color/cpp/temperature/temperature_cache_test.cc @@ -0,0 +1,115 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/temperature/temperature_cache.h" + +#include + +#include "testing/base/public/gunit.h" +#include "cpp/cam/hct.h" + +namespace material_color_utilities { + +namespace { + +TEST(TemperatureCacheTest, RawTemperature) { + Hct blue_hct(0xff0000ff); + double blue_temp = TemperatureCache::RawTemperature(blue_hct); + EXPECT_NEAR(-1.393, blue_temp, 0.001); + + Hct red_hct(0xffff0000); + double red_temp = TemperatureCache::RawTemperature(red_hct); + EXPECT_NEAR(2.351, red_temp, 0.001); + + Hct green_hct(0xff00ff00); + double green_temp = TemperatureCache::RawTemperature(green_hct); + EXPECT_NEAR(-0.267, green_temp, 0.001); + + Hct white_hct(0xffffffff); + double white_temp = TemperatureCache::RawTemperature(white_hct); + EXPECT_NEAR(-0.5, white_temp, 0.001); + + Hct black_hct(0xff000000); + double black_temp = TemperatureCache::RawTemperature(black_hct); + EXPECT_NEAR(-0.5, black_temp, 0.001); +} + +TEST(TemperatureCacheTest, Complement) { + unsigned int blue_complement = + TemperatureCache(Hct(0xff0000ff)).GetComplement().ToInt(); + EXPECT_EQ(0xff9d0002, blue_complement); + + unsigned int red_complement = + TemperatureCache(Hct(0xffff0000)).GetComplement().ToInt(); + EXPECT_EQ(0xff007bfc, red_complement); + + unsigned int green_complement = + TemperatureCache(Hct(0xff00ff00)).GetComplement().ToInt(); + EXPECT_EQ(0xffffd2c9, green_complement); + + unsigned int white_complement = + TemperatureCache(Hct(0xffffffff)).GetComplement().ToInt(); + EXPECT_EQ(0xffffffff, white_complement); + + unsigned int black_complement = + TemperatureCache(Hct(0xff000000)).GetComplement().ToInt(); + EXPECT_EQ(0xff000000, black_complement); +} + +TEST(TemperatureCacheTest, Analogous) { + std::vector blue_analogous = + TemperatureCache(Hct(0xff0000ff)).GetAnalogousColors(); + EXPECT_EQ(0xff00590c, blue_analogous.at(0).ToInt()); + EXPECT_EQ(0xff00564e, blue_analogous.at(1).ToInt()); + EXPECT_EQ(0xff0000ff, blue_analogous.at(2).ToInt()); + EXPECT_EQ(0xff6700cc, blue_analogous.at(3).ToInt()); + EXPECT_EQ(0xff81009f, blue_analogous.at(4).ToInt()); + + std::vector red_analogous = + TemperatureCache(Hct(0xffff0000)).GetAnalogousColors(); + EXPECT_EQ(0xfff60082, red_analogous.at(0).ToInt()); + EXPECT_EQ(0xfffc004c, red_analogous.at(1).ToInt()); + EXPECT_EQ(0xffff0000, red_analogous.at(2).ToInt()); + EXPECT_EQ(0xffd95500, red_analogous.at(3).ToInt()); + EXPECT_EQ(0xffaf7200, red_analogous.at(4).ToInt()); + + std::vector green_analogous = + TemperatureCache(Hct(0xff00ff00)).GetAnalogousColors(); + EXPECT_EQ(0xffcee900, green_analogous.at(0).ToInt()); + EXPECT_EQ(0xff92f500, green_analogous.at(1).ToInt()); + EXPECT_EQ(0xff00ff00, green_analogous.at(2).ToInt()); + EXPECT_EQ(0xff00fd6f, green_analogous.at(3).ToInt()); + EXPECT_EQ(0xff00fab3, green_analogous.at(4).ToInt()); + + std::vector black_analogous = + TemperatureCache(Hct(0xff000000)).GetAnalogousColors(); + EXPECT_EQ(0xff000000, black_analogous.at(0).ToInt()); + EXPECT_EQ(0xff000000, black_analogous.at(1).ToInt()); + EXPECT_EQ(0xff000000, black_analogous.at(2).ToInt()); + EXPECT_EQ(0xff000000, black_analogous.at(3).ToInt()); + EXPECT_EQ(0xff000000, black_analogous.at(4).ToInt()); + + std::vector white_analogous = + TemperatureCache(Hct(0xffffffff)).GetAnalogousColors(); + EXPECT_EQ(0xffffffff, white_analogous.at(0).ToInt()); + EXPECT_EQ(0xffffffff, white_analogous.at(1).ToInt()); + EXPECT_EQ(0xffffffff, white_analogous.at(2).ToInt()); + EXPECT_EQ(0xffffffff, white_analogous.at(3).ToInt()); + EXPECT_EQ(0xffffffff, white_analogous.at(4).ToInt()); +} + +} // namespace +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/utils/utils.cc b/third_party/material-color/cpp/utils/utils.cc new file mode 100644 index 0000000..4a0903e --- /dev/null +++ b/third_party/material-color/cpp/utils/utils.cc @@ -0,0 +1,188 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/utils/utils.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +// #include "absl/strings/str_cat.h" + +namespace material_color_utilities +{ + +int RedFromInt(const Argb argb) { return (argb & 0x00ff0000) >> 16; } + +int GreenFromInt(const Argb argb) { return (argb & 0x0000ff00) >> 8; } + +int BlueFromInt(const Argb argb) { return (argb & 0x000000ff); } + +Argb ArgbFromRgb(const int red, const int green, const int blue) { + return 0xFF000000 | ((red & 0xff) << 16) | ((green & 0xff) << 8) | (blue & 0xff); +} + +// Converts a color from linear RGB components to ARGB format. +Argb ArgbFromLinrgb(Vec3 linrgb) { + int r = Delinearized(linrgb.a); + int g = Delinearized(linrgb.b); + int b = Delinearized(linrgb.c); + + return 0xFF000000 | ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff); +} + +int Delinearized(const double rgb_component) { + double normalized = rgb_component / 100; + double delinearized; + if (normalized <= 0.0031308) { + delinearized = normalized * 12.92; + } else { + delinearized = 1.055 * std::pow(normalized, 1.0 / 2.4) - 0.055; + } + return std::clamp((int)round(delinearized * 255.0), 0, 255); +} + +double Linearized(const int rgb_component) { + double normalized = rgb_component / 255.0; + if (normalized <= 0.040449936) { + return normalized / 12.92 * 100.0; + } else { + return std::pow((normalized + 0.055) / 1.055, 2.4) * 100.0; + } +} + +int AlphaFromInt(Argb argb) { return (argb & 0xff000000) >> 24; } + +bool IsOpaque(Argb argb) { return AlphaFromInt(argb) == 255; } + +double LstarFromArgb(Argb argb) { + // xyz from argb + int red = (argb & 0x00ff0000) >> 16; + int green = (argb & 0x0000ff00) >> 8; + int blue = (argb & 0x000000ff); + double red_l = Linearized(red); + double green_l = Linearized(green); + double blue_l = Linearized(blue); + double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l; + return LstarFromY(y); +} + +double YFromLstar(double lstar) { + static const double ke = 8.0; + if (lstar > ke) { + double cube_root = (lstar + 16.0) / 116.0; + double cube = cube_root * cube_root * cube_root; + return cube * 100.0; + } else { + return lstar / (24389.0 / 27.0) * 100.0; + } +} + +double LstarFromY(double y) { + static const double e = 216.0 / 24389.0; + double yNormalized = y / 100.0; + if (yNormalized <= e) { + return (24389.0 / 27.0) * yNormalized; + } else { + return 116.0 * std::pow(yNormalized, 1.0 / 3.0) - 16.0; + } +} + +int SanitizeDegreesInt(const int degrees) { + if (degrees < 0) { + return (degrees % 360) + 360; + } else if (degrees >= 360.0) { + return degrees % 360; + } else { + return degrees; + } +} + +// Sanitizes a degree measure as a floating-point number. +// +// Returns a degree measure between 0.0 (inclusive) and 360.0 (exclusive). +double SanitizeDegreesDouble(const double degrees) { + if (degrees < 0.0) { + return fmod(degrees, 360.0) + 360; + } else if (degrees >= 360.0) { + return fmod(degrees, 360.0); + } else { + return degrees; + } +} + +double DiffDegrees(const double a, const double b) { return 180.0 - abs(abs(a - b) - 180.0); } + +double RotationDirection(const double from, const double to) { + double increasing_difference = SanitizeDegreesDouble(to - from); + return increasing_difference <= 180.0 ? 1.0 : -1.0; +} + +// Converts a color in ARGB format to a hexadecimal string in lowercase. +// +// For instance: hex_from_argb(0xff012345) == "ff012345" +std::string HexFromArgb(Argb argb) { + // return absl::StrCat(absl::Hex(argb)); + constexpr std::string_view hex_digits = "0123456789abcdef"; + std::string out; + for (int i = 0; i < 4; i++) { + auto b = std::byte { (unsigned char)((argb >> i) & 0xff) }; + + auto idx = std::to_integer((b << 4) >> 4); + out.push_back(hex_digits[idx]); + idx = std::to_integer(b >> 4); + out.push_back(hex_digits[idx]); + } + std::reverse(out.begin(), out.end()); + return out; +} + +Argb IntFromLstar(const double lstar) { + double y = YFromLstar(lstar); + int component = Delinearized(y); + return ArgbFromRgb(component, component, component); +} + +// The signum function. +// +// Returns 1 if num > 0, -1 if num < 0, and 0 if num = 0 +int Signum(double num) { + if (num < 0) { + return -1; + } else if (num == 0) { + return 0; + } else { + return 1; + } +} + +double Lerp(double start, double stop, double amount) { + return (1.0 - amount) * start + amount * stop; +} + +Vec3 MatrixMultiply(Vec3 input, const double matrix[3][3]) { + double a = input.a * matrix[0][0] + input.b * matrix[0][1] + input.c * matrix[0][2]; + double b = input.a * matrix[1][0] + input.b * matrix[1][1] + input.c * matrix[1][2]; + double c = input.a * matrix[2][0] + input.b * matrix[2][1] + input.c * matrix[2][2]; + return (Vec3) { a, b, c }; +} +} // namespace material_color_utilities diff --git a/third_party/material-color/cpp/utils/utils.h b/third_party/material-color/cpp/utils/utils.h new file mode 100644 index 0000000..c890895 --- /dev/null +++ b/third_party/material-color/cpp/utils/utils.h @@ -0,0 +1,211 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CPP_UTILS_UTILS_H_ +#define CPP_UTILS_UTILS_H_ + +#include +#include + +namespace material_color_utilities { + +using Argb = uint32_t; + +/** + * A vector with three floating-point numbers as components. + */ +struct Vec3 { + double a = 0.0; + double b = 0.0; + double c = 0.0; +}; + +/** + * Value of pi. + */ +inline constexpr double kPi = 3.141592653589793; + +/** + * Returns the standard white point; white on a sunny day. + */ +inline constexpr double kWhitePointD65[] = {95.047, 100.0, 108.883}; + +/** + * Returns the red component of a color in ARGB format. + */ +int RedFromInt(const Argb argb); + +/** + * Returns the green component of a color in ARGB format. + */ +int GreenFromInt(const Argb argb); + +/** + * Returns the blue component of a color in ARGB format. + */ +int BlueFromInt(const Argb argb); + +/** + * Returns the alpha component of a color in ARGB format. + */ +int AlphaFromInt(const Argb argb); + +/** + * Converts a color from RGB components to ARGB format. + */ +Argb ArgbFromRgb(const int red, const int green, const int blue); + +/** + * Converts a color from linear RGB components to ARGB format. + */ +Argb ArgbFromLinrgb(Vec3 linrgb); + +/** + * Returns whether a color in ARGB format is opaque. + */ +bool IsOpaque(const Argb argb); + +/** + * Sanitizes a degree measure as an integer. + * + * @return a degree measure between 0 (inclusive) and 360 (exclusive). + */ +int SanitizeDegreesInt(const int degrees); + +/** + * Sanitizes a degree measure as an floating-point number. + * + * @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive). + */ +double SanitizeDegreesDouble(const double degrees); + +/** + * Distance of two points on a circle, represented using degrees. + */ +double DiffDegrees(const double a, const double b); + +/** + * Sign of direction change needed to travel from one angle to + * another. + * + * For angles that are 180 degrees apart from each other, both + * directions have the same travel distance, so either direction is + * shortest. The value 1.0 is returned in this case. + * + * @param from The angle travel starts from, in degrees. + * + * @param to The angle travel ends at, in degrees. + * + * @return -1 if decreasing from leads to the shortest travel + * distance, 1 if increasing from leads to the shortest travel + * distance. + */ +double RotationDirection(const double from, const double to); + +/** + * Computes the L* value of a color in ARGB representation. + * + * @param argb ARGB representation of a color + * + * @return L*, from L*a*b*, coordinate of the color + */ +double LstarFromArgb(const Argb argb); + +/** + * Returns the hexadecimal representation of a color. + */ +std::string HexFromArgb(Argb argb); + +/** + * Linearizes an RGB component. + * + * @param rgb_component 0 <= rgb_component <= 255, represents R/G/B + * channel + * + * @return 0.0 <= output <= 100.0, color channel converted to + * linear RGB space + */ +double Linearized(const int rgb_component); + +/** + * Delinearizes an RGB component. + * + * @param rgb_component 0.0 <= rgb_component <= 100.0, represents linear + * R/G/B channel + * + * @return 0 <= output <= 255, color channel converted to regular + * RGB space + */ +int Delinearized(const double rgb_component); + +/** + * Converts an L* value to a Y value. + * + * L* in L*a*b* and Y in XYZ measure the same quantity, luminance. + * + * L* measures perceptual luminance, a linear scale. Y in XYZ + * measures relative luminance, a logarithmic scale. + * + * @param lstar L* in L*a*b*. 0.0 <= L* <= 100.0 + * + * @return Y in XYZ. 0.0 <= Y <= 100.0 + */ +double YFromLstar(const double lstar); + +/** + * Converts a Y value to an L* value. + * + * L* in L*a*b* and Y in XYZ measure the same quantity, luminance. + * + * L* measures perceptual luminance, a linear scale. Y in XYZ + * measures relative luminance, a logarithmic scale. + * + * @param y Y in XYZ. 0.0 <= Y <= 100.0 + * + * @return L* in L*a*b*. 0.0 <= L* <= 100.0 + */ +double LstarFromY(const double y); + +/** + * Converts an L* value to an ARGB representation. + * + * @param lstar L* in L*a*b*. 0.0 <= L* <= 100.0 + * + * @return ARGB representation of grayscale color with lightness matching L* + */ +Argb IntFromLstar(const double lstar); + +/** + * The signum function. + * + * @return 1 if num > 0, -1 if num < 0, and 0 if num = 0 + */ +int Signum(double num); + +/** + * The linear interpolation function. + * + * @return start if amount = 0 and stop if amount = 1 + */ +double Lerp(double start, double stop, double amount); + +/** + * Multiplies a 1x3 row vector with a 3x3 matrix, returning the product. + */ +Vec3 MatrixMultiply(Vec3 input, const double matrix[3][3]); + +} // namespace material_color_utilities +#endif // CPP_UTILS_UTILS_H_ diff --git a/third_party/material-color/cpp/utils/utils_test.cc b/third_party/material-color/cpp/utils/utils_test.cc new file mode 100644 index 0000000..9f415ee --- /dev/null +++ b/third_party/material-color/cpp/utils/utils_test.cc @@ -0,0 +1,348 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cpp/utils/utils.h" + +#include + +#include "testing/base/public/gmock.h" +#include "testing/base/public/gunit.h" + +namespace material_color_utilities { + +namespace { + +using testing::DoubleNear; + +constexpr double kMatrix[3][3] = { + {1, 2, 3}, + {-4, 5, -6}, + {-7, -8, -9}, +}; + +TEST(UtilsTest, Signum) { + EXPECT_EQ(Signum(0.001), 1); + EXPECT_EQ(Signum(3.0), 1); + EXPECT_EQ(Signum(100.0), 1); + EXPECT_EQ(Signum(-0.002), -1); + EXPECT_EQ(Signum(-4.0), -1); + EXPECT_EQ(Signum(-101.0), -1); + EXPECT_EQ(Signum(0.0), 0); +} + +TEST(UtilsTest, RotationIsPositiveForCounterclockwise) { + EXPECT_EQ(RotationDirection(0.0, 30.0), 1.0); + EXPECT_EQ(RotationDirection(0.0, 60.0), 1.0); + EXPECT_EQ(RotationDirection(0.0, 150.0), 1.0); + EXPECT_EQ(RotationDirection(90.0, 240.0), 1.0); + EXPECT_EQ(RotationDirection(300.0, 30.0), 1.0); + EXPECT_EQ(RotationDirection(270.0, 60.0), 1.0); + EXPECT_EQ(RotationDirection(360.0 * 2, 15.0), 1.0); + EXPECT_EQ(RotationDirection(360.0 * 3 + 15.0, -360.0 * 4 + 30.0), 1.0); +} + +TEST(UtilsTest, RotationIsNegativeForClockwise) { + EXPECT_EQ(RotationDirection(30.0, 0.0), -1.0); + EXPECT_EQ(RotationDirection(60.0, 0.0), -1.0); + EXPECT_EQ(RotationDirection(150.0, 0.0), -1.0); + EXPECT_EQ(RotationDirection(240.0, 90.0), -1.0); + EXPECT_EQ(RotationDirection(30.0, 300.0), -1.0); + EXPECT_EQ(RotationDirection(60.0, 270.0), -1.0); + EXPECT_EQ(RotationDirection(15.0, -360.0 * 2), -1.0); + EXPECT_EQ(RotationDirection(-360.0 * 4 + 270.0, 360.0 * 5 + 180.0), -1.0); +} + +TEST(UtilsTest, AngleDifference) { + EXPECT_EQ(DiffDegrees(0.0, 30.0), 30.0); + EXPECT_EQ(DiffDegrees(0.0, 60.0), 60.0); + EXPECT_EQ(DiffDegrees(0.0, 150.0), 150.0); + EXPECT_EQ(DiffDegrees(90.0, 240.0), 150.0); + EXPECT_EQ(DiffDegrees(300.0, 30.0), 90.0); + EXPECT_EQ(DiffDegrees(270.0, 60.0), 150.0); + + EXPECT_EQ(DiffDegrees(30.0, 0.0), 30.0); + EXPECT_EQ(DiffDegrees(60.0, 0.0), 60.0); + EXPECT_EQ(DiffDegrees(150.0, 0.0), 150.0); + EXPECT_EQ(DiffDegrees(240.0, 90.0), 150.0); + EXPECT_EQ(DiffDegrees(30.0, 300.0), 90.0); + EXPECT_EQ(DiffDegrees(60.0, 270.0), 150.0); +} + +TEST(UtilsTest, AngleSanitation) { + EXPECT_EQ(SanitizeDegreesInt(30), 30); + EXPECT_EQ(SanitizeDegreesInt(240), 240); + EXPECT_EQ(SanitizeDegreesInt(360), 0); + EXPECT_EQ(SanitizeDegreesInt(-30), 330); + EXPECT_EQ(SanitizeDegreesInt(-750), 330); + EXPECT_EQ(SanitizeDegreesInt(-54321), 39); + + EXPECT_THAT(SanitizeDegreesDouble(30.0), DoubleNear(30.0, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(240.0), DoubleNear(240.0, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(360.0), DoubleNear(0.0, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(-30.0), DoubleNear(330.0, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(-750.0), DoubleNear(330.0, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(-54321.0), DoubleNear(39.0, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(360.125), DoubleNear(0.125, 1e-4)); + EXPECT_THAT(SanitizeDegreesDouble(-11111.11), DoubleNear(48.89, 1e-4)); +} + +TEST(UtilsTest, MatrixMultiply) { + Vec3 vector_one = MatrixMultiply({1, 3, 5}, kMatrix); + EXPECT_THAT(vector_one.a, DoubleNear(22, 1e-4)); + EXPECT_THAT(vector_one.b, DoubleNear(-19, 1e-4)); + EXPECT_THAT(vector_one.c, DoubleNear(-76, 1e-4)); + + Vec3 vector_two = MatrixMultiply({-11.1, 22.2, -33.3}, kMatrix); + EXPECT_THAT(vector_two.a, DoubleNear(-66.6, 1e-4)); + EXPECT_THAT(vector_two.b, DoubleNear(355.2, 1e-4)); + EXPECT_THAT(vector_two.c, DoubleNear(199.8, 1e-4)); +} + +TEST(UtilsTest, AlphaFromInt) { + EXPECT_EQ(AlphaFromInt(0xff123456), 0xff); + EXPECT_EQ(AlphaFromInt(0xffabcdef), 0xff); +} + +TEST(UtilsTest, RedFromInt) { + EXPECT_EQ(RedFromInt(0xff123456), 0x12); + EXPECT_EQ(RedFromInt(0xffabcdef), 0xab); +} + +TEST(UtilsTest, GreenFromInt) { + EXPECT_EQ(GreenFromInt(0xff123456), 0x34); + EXPECT_EQ(GreenFromInt(0xffabcdef), 0xcd); +} + +TEST(UtilsTest, BlueFromInt) { + EXPECT_EQ(BlueFromInt(0xff123456), 0x56); + EXPECT_EQ(BlueFromInt(0xffabcdef), 0xef); +} + +TEST(UtilsTest, Opaqueness) { + EXPECT_TRUE(IsOpaque(0xff123456)); + EXPECT_FALSE(IsOpaque(0xf0123456)); + EXPECT_FALSE(IsOpaque(0x00123456)); +} + +TEST(UtilsTest, LinearizedComponents) { + EXPECT_THAT(Linearized(0), DoubleNear(0.0, 1e-4)); + EXPECT_THAT(Linearized(1), DoubleNear(0.0303527, 1e-4)); + EXPECT_THAT(Linearized(2), DoubleNear(0.0607054, 1e-4)); + EXPECT_THAT(Linearized(8), DoubleNear(0.242822, 1e-4)); + EXPECT_THAT(Linearized(9), DoubleNear(0.273174, 1e-4)); + EXPECT_THAT(Linearized(16), DoubleNear(0.518152, 1e-4)); + EXPECT_THAT(Linearized(32), DoubleNear(1.44438, 1e-4)); + EXPECT_THAT(Linearized(64), DoubleNear(5.12695, 1e-4)); + EXPECT_THAT(Linearized(128), DoubleNear(21.5861, 1e-4)); + EXPECT_THAT(Linearized(255), DoubleNear(100.0, 1e-4)); +} + +TEST(UtilsTest, DelinearizedComponents) { + EXPECT_EQ(Delinearized(0.0), 0); + EXPECT_EQ(Delinearized(0.0303527), 1); + EXPECT_EQ(Delinearized(0.0607054), 2); + EXPECT_EQ(Delinearized(0.242822), 8); + EXPECT_EQ(Delinearized(0.273174), 9); + EXPECT_EQ(Delinearized(0.518152), 16); + EXPECT_EQ(Delinearized(1.44438), 32); + EXPECT_EQ(Delinearized(5.12695), 64); + EXPECT_EQ(Delinearized(21.5861), 128); + EXPECT_EQ(Delinearized(100.0), 255); + + EXPECT_EQ(Delinearized(25.0), 137); + EXPECT_EQ(Delinearized(50.0), 188); + EXPECT_EQ(Delinearized(75.0), 225); + + // Delinearized clamps out-of-range inputs. + EXPECT_EQ(Delinearized(-1.0), 0); + EXPECT_EQ(Delinearized(-10000.0), 0); + EXPECT_EQ(Delinearized(101.0), 255); + EXPECT_EQ(Delinearized(10000.0), 255); +} + +TEST(UtilsTest, DelinearizedIsLeftInverseOfLinearized) { + EXPECT_EQ(Delinearized(Linearized(0)), 0); + EXPECT_EQ(Delinearized(Linearized(1)), 1); + EXPECT_EQ(Delinearized(Linearized(2)), 2); + EXPECT_EQ(Delinearized(Linearized(8)), 8); + EXPECT_EQ(Delinearized(Linearized(9)), 9); + EXPECT_EQ(Delinearized(Linearized(16)), 16); + EXPECT_EQ(Delinearized(Linearized(32)), 32); + EXPECT_EQ(Delinearized(Linearized(64)), 64); + EXPECT_EQ(Delinearized(Linearized(128)), 128); + EXPECT_EQ(Delinearized(Linearized(255)), 255); +} + +TEST(UtilsTest, ArgbFromLinrgb) { + EXPECT_EQ(static_cast(ArgbFromLinrgb({25.0, 50.0, 75.0})), + 0xff89bce1); + EXPECT_EQ(static_cast(ArgbFromLinrgb({0.03, 0.06, 0.12})), + 0xff010204); +} + +TEST(UtilsTest, LstarFromArgb) { + EXPECT_THAT(LstarFromArgb(0xff89bce1), DoubleNear(74.011, 1e-4)); + EXPECT_THAT(LstarFromArgb(0xff010204), DoubleNear(0.529651, 1e-4)); +} + +TEST(UtilsTest, HexFromArgb) { + EXPECT_EQ(HexFromArgb(0xff89bce1), "ff89bce1"); + EXPECT_EQ(HexFromArgb(0xff010204), "ff010204"); +} + +TEST(UtilsTest, IntFromLstar) { + // Given an L* brightness value in [0, 100], IntFromLstar returns a greyscale + // color in ARGB format with that brightness. + // For L* outside the domain [0, 100], returns black or white. + + EXPECT_EQ(static_cast(IntFromLstar(0.0)), 0xff000000); + EXPECT_EQ(static_cast(IntFromLstar(0.25)), 0xff010101); + EXPECT_EQ(static_cast(IntFromLstar(0.5)), 0xff020202); + EXPECT_EQ(static_cast(IntFromLstar(1.0)), 0xff040404); + EXPECT_EQ(static_cast(IntFromLstar(2.0)), 0xff070707); + EXPECT_EQ(static_cast(IntFromLstar(4.0)), 0xff0e0e0e); + EXPECT_EQ(static_cast(IntFromLstar(8.0)), 0xff181818); + EXPECT_EQ(static_cast(IntFromLstar(25.0)), 0xff3b3b3b); + EXPECT_EQ(static_cast(IntFromLstar(50.0)), 0xff777777); + EXPECT_EQ(static_cast(IntFromLstar(75.0)), 0xffb9b9b9); + EXPECT_EQ(static_cast(IntFromLstar(99.0)), 0xfffcfcfc); + EXPECT_EQ(static_cast(IntFromLstar(100.0)), 0xffffffff); + + EXPECT_EQ(static_cast(IntFromLstar(-1.0)), 0xff000000); + EXPECT_EQ(static_cast(IntFromLstar(-2.0)), 0xff000000); + EXPECT_EQ(static_cast(IntFromLstar(-3.0)), 0xff000000); + EXPECT_EQ(static_cast(IntFromLstar(-9999999.0)), 0xff000000); + + EXPECT_EQ(static_cast(IntFromLstar(101.0)), 0xffffffff); + EXPECT_EQ(static_cast(IntFromLstar(111.0)), 0xffffffff); + EXPECT_EQ(static_cast(IntFromLstar(9999999.0)), 0xffffffff); +} + +TEST(UtilsTest, LstarArgbRoundtripProperty) { + // Confirms that L* -> ARGB -> L* preserves original value + // (taking ARGB rounding into consideration). + EXPECT_THAT(LstarFromArgb(IntFromLstar(0.0)), DoubleNear(0.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(1.0)), DoubleNear(1.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(2.0)), DoubleNear(2.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(8.0)), DoubleNear(8.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(25.0)), DoubleNear(25.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(50.0)), DoubleNear(50.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(75.0)), DoubleNear(75.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(99.0)), DoubleNear(99.0, 1.0)); + EXPECT_THAT(LstarFromArgb(IntFromLstar(100.0)), DoubleNear(100.0, 1.0)); +} + +TEST(UtilsTest, ArgbLstarRoundtripProperty) { + // Confirms that ARGB -> L* -> ARGB preserves original value + // for greyscale colors. + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xff000000))), + 0xff000000); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xff010101))), + 0xff010101); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xff020202))), + 0xff020202); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xff111111))), + 0xff111111); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xff333333))), + 0xff333333); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xff777777))), + 0xff777777); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xffbbbbbb))), + 0xffbbbbbb); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xfffefefe))), + 0xfffefefe); + EXPECT_EQ(static_cast(IntFromLstar(LstarFromArgb(0xffffffff))), + 0xffffffff); +} + +TEST(UtilsTest, YFromLstar) { + EXPECT_THAT(YFromLstar(0.0), DoubleNear(0.0, 1e-5)); + EXPECT_THAT(YFromLstar(0.1), DoubleNear(0.0110705, 1e-5)); + EXPECT_THAT(YFromLstar(0.2), DoubleNear(0.0221411, 1e-5)); + EXPECT_THAT(YFromLstar(0.3), DoubleNear(0.0332116, 1e-5)); + EXPECT_THAT(YFromLstar(0.4), DoubleNear(0.0442822, 1e-5)); + EXPECT_THAT(YFromLstar(0.5), DoubleNear(0.0553528, 1e-5)); + EXPECT_THAT(YFromLstar(1.0), DoubleNear(0.1107056, 1e-5)); + EXPECT_THAT(YFromLstar(2.0), DoubleNear(0.2214112, 1e-5)); + EXPECT_THAT(YFromLstar(3.0), DoubleNear(0.3321169, 1e-5)); + EXPECT_THAT(YFromLstar(4.0), DoubleNear(0.4428225, 1e-5)); + EXPECT_THAT(YFromLstar(5.0), DoubleNear(0.5535282, 1e-5)); + EXPECT_THAT(YFromLstar(8.0), DoubleNear(0.8856451, 1e-5)); + EXPECT_THAT(YFromLstar(10.0), DoubleNear(1.1260199, 1e-5)); + EXPECT_THAT(YFromLstar(15.0), DoubleNear(1.9085832, 1e-5)); + EXPECT_THAT(YFromLstar(20.0), DoubleNear(2.9890524, 1e-5)); + EXPECT_THAT(YFromLstar(25.0), DoubleNear(4.4154767, 1e-5)); + EXPECT_THAT(YFromLstar(30.0), DoubleNear(6.2359055, 1e-5)); + EXPECT_THAT(YFromLstar(40.0), DoubleNear(11.2509737, 1e-5)); + EXPECT_THAT(YFromLstar(50.0), DoubleNear(18.4186518, 1e-5)); + EXPECT_THAT(YFromLstar(60.0), DoubleNear(28.1233342, 1e-5)); + EXPECT_THAT(YFromLstar(70.0), DoubleNear(40.7494157, 1e-5)); + EXPECT_THAT(YFromLstar(80.0), DoubleNear(56.6812907, 1e-5)); + EXPECT_THAT(YFromLstar(90.0), DoubleNear(76.3033539, 1e-5)); + EXPECT_THAT(YFromLstar(95.0), DoubleNear(87.6183294, 1e-5)); + EXPECT_THAT(YFromLstar(99.0), DoubleNear(97.4360239, 1e-5)); + EXPECT_THAT(YFromLstar(100.0), DoubleNear(100.0, 1e-5)); +} + +TEST(UtilsTest, LstarFromY) { + EXPECT_THAT(LstarFromY(0.0), DoubleNear(0.0, 1e-5)); + EXPECT_THAT(LstarFromY(0.1), DoubleNear(0.9032962, 1e-5)); + EXPECT_THAT(LstarFromY(0.2), DoubleNear(1.8065925, 1e-5)); + EXPECT_THAT(LstarFromY(0.3), DoubleNear(2.7098888, 1e-5)); + EXPECT_THAT(LstarFromY(0.4), DoubleNear(3.6131851, 1e-5)); + EXPECT_THAT(LstarFromY(0.5), DoubleNear(4.5164814, 1e-5)); + EXPECT_THAT(LstarFromY(0.8856451), DoubleNear(8.0, 1e-5)); + EXPECT_THAT(LstarFromY(1.0), DoubleNear(8.9914424, 1e-5)); + EXPECT_THAT(LstarFromY(2.0), DoubleNear(15.4872443, 1e-5)); + EXPECT_THAT(LstarFromY(3.0), DoubleNear(20.0438970, 1e-5)); + EXPECT_THAT(LstarFromY(4.0), DoubleNear(23.6714419, 1e-5)); + EXPECT_THAT(LstarFromY(5.0), DoubleNear(26.7347653, 1e-5)); + EXPECT_THAT(LstarFromY(10.0), DoubleNear(37.8424304, 1e-5)); + EXPECT_THAT(LstarFromY(15.0), DoubleNear(45.6341970, 1e-5)); + EXPECT_THAT(LstarFromY(20.0), DoubleNear(51.8372115, 1e-5)); + EXPECT_THAT(LstarFromY(25.0), DoubleNear(57.0754208, 1e-5)); + EXPECT_THAT(LstarFromY(30.0), DoubleNear(61.6542222, 1e-5)); + EXPECT_THAT(LstarFromY(40.0), DoubleNear(69.4695307, 1e-5)); + EXPECT_THAT(LstarFromY(50.0), DoubleNear(76.0692610, 1e-5)); + EXPECT_THAT(LstarFromY(60.0), DoubleNear(81.8381891, 1e-5)); + EXPECT_THAT(LstarFromY(70.0), DoubleNear(86.9968642, 1e-5)); + EXPECT_THAT(LstarFromY(80.0), DoubleNear(91.6848609, 1e-5)); + EXPECT_THAT(LstarFromY(90.0), DoubleNear(95.9967686, 1e-5)); + EXPECT_THAT(LstarFromY(95.0), DoubleNear(98.0335184, 1e-5)); + EXPECT_THAT(LstarFromY(99.0), DoubleNear(99.6120372, 1e-5)); + EXPECT_THAT(LstarFromY(100.0), DoubleNear(100.0, 1e-5)); +} + +TEST(UtilsTest, YLstarRoundtripProperty) { + // Confirms that Y -> L* -> Y preserves original value. + for (double y = 0.0; y <= 100.0; y += 0.1) { + double lstar = LstarFromY(y); + double reconstructedY = YFromLstar(lstar); + EXPECT_THAT(reconstructedY, DoubleNear(y, 1e-8)); + } +} + +TEST(UtilsTest, LstarYRoundtripProperty) { + // Confirms that L* -> Y -> L* preserves original value. + for (double lstar = 0.0; lstar <= 100.0; lstar += 0.1) { + double y = YFromLstar(lstar); + double reconstructedLstar = LstarFromY(y); + EXPECT_THAT(reconstructedLstar, DoubleNear(lstar, 1e-8)); + } +} + +} // namespace +} // namespace material_color_utilities