// // Created by vlad on 2/4/24. // #pragma once #include "omath/linear_algebra/vector4.hpp" #include namespace omath { struct Hsv final { float hue{}; float saturation{}; float value{}; }; class Color final { Vector4 m_value; public: constexpr const Vector4& value() const { return m_value; } constexpr Color(const float r, const float g, const float b, const float a) noexcept: m_value(r, g, b, a) { m_value.clamp(0.f, 1.f); } constexpr explicit Color(const Vector4& value) : m_value(value) { m_value.clamp(0.f, 1.f); } constexpr explicit Color() noexcept = default; [[nodiscard]] constexpr static Color from_rgba(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) noexcept { return Color(Vector4(r, g, b, a) / 255.f); } [[nodiscard]] constexpr static Color from_hsv(float hue, const float saturation, const float value) noexcept { float r{}, g{}, b{}; hue = std::clamp(hue, 0.f, 1.f); const int i = static_cast(hue * 6.f); const float f = hue * 6.f - static_cast(i); const float p = value * (1 - saturation); const float q = value * (1 - f * saturation); const float t = value * (1 - (1 - f) * saturation); switch (i % 6) { case 0: r = value, g = t, b = p; break; case 1: r = q, g = value, b = p; break; case 2: r = p, g = value, b = t; break; case 3: r = p, g = q, b = value; break; case 4: r = t, g = p, b = value; break; case 5: r = value, g = p, b = q; break; default: std::unreachable(); } return {r, g, b, 1.f}; } [[nodiscard]] constexpr static Color from_hsv(const Hsv& hsv) noexcept { return from_hsv(hsv.hue, hsv.saturation, hsv.value); } [[nodiscard]] constexpr Hsv to_hsv() const noexcept { Hsv hsv_data; const float& red = m_value.x; const float& green = m_value.y; const float& blue = m_value.z; const float max = std::max({red, green, blue}); const float min = std::min({red, green, blue}); const float delta = max - min; if (delta == 0.f) hsv_data.hue = 0.f; else if (max == red) hsv_data.hue = 60.f * (std::fmod(static_cast((green - blue) / delta), 6.f)); else if (max == green) hsv_data.hue = 60.f * (((blue - red) / delta) + 2.f); else if (max == blue) hsv_data.hue = 60.f * (((red - green) / delta) + 4.f); if (hsv_data.hue < 0.f) hsv_data.hue += 360.f; hsv_data.hue /= 360.f; hsv_data.saturation = max == 0.f ? 0.f : delta / max; hsv_data.value = max; return hsv_data; } constexpr void set_hue(const float hue) noexcept { auto hsv = to_hsv(); hsv.hue = hue; *this = from_hsv(hsv); } constexpr void set_saturation(const float saturation) noexcept { auto hsv = to_hsv(); hsv.saturation = saturation; *this = from_hsv(hsv); } constexpr void set_value(const float value) noexcept { auto hsv = to_hsv(); hsv.value = value; *this = from_hsv(hsv); } [[nodiscard]] constexpr Color blend(const Color& other, float ratio) const noexcept { ratio = std::clamp(ratio, 0.f, 1.f); return Color(this->m_value * (1.f - ratio) + other.m_value * ratio); } [[nodiscard]] static constexpr Color red() { return {1.f, 0.f, 0.f, 1.f}; } [[nodiscard]] static constexpr Color green() { return {0.f, 1.f, 0.f, 1.f}; } [[nodiscard]] static constexpr Color blue() { return {0.f, 0.f, 1.f, 1.f}; } #ifdef OMATH_IMGUI_INTEGRATION [[nodiscard]] ImColor to_im_color() const noexcept { return {m_value.to_im_vec4()}; } #endif [[nodiscard]] std::string to_string() const noexcept { return std::format("[r:{}, g:{}, b:{}, a:{}]", static_cast(m_value.x * 255.f), static_cast(m_value.y * 255.f), static_cast(m_value.z * 255.f), static_cast(m_value.w * 255.f)); } [[nodiscard]] std::string to_rgbf_string() const noexcept { return std::format("[r:{}, g:{}, b:{}, a:{}]", m_value.x, m_value.y, m_value.z, m_value.w); } [[nodiscard]] std::string to_hsv_string() const noexcept { const auto [hue, saturation, value] = to_hsv(); return std::format("[h:{}, s:{}, v:{}]", hue, saturation, value); } [[nodiscard]] std::wstring to_wstring() const noexcept { const auto ascii_string = to_string(); return {ascii_string.cbegin(), ascii_string.cend()}; } // ReSharper disable once CppInconsistentNaming [[nodiscard]] std::u8string to_u8string() const noexcept { const auto ascii_string = to_string(); return {ascii_string.cbegin(), ascii_string.cend()}; } }; } // namespace omath template<> struct std::formatter // NOLINT(*-dcl58-cpp) { enum class ColorFormat { rgb, rgbf, hsv }; ColorFormat color_format = ColorFormat::rgb; constexpr auto parse(std::format_parse_context& ctx) { const auto it = ctx.begin(); const auto end = ctx.end(); if (it == end || *it == '}') return it; const std::string_view spec(it, end); if (spec.starts_with("rgbf")) { color_format = ColorFormat::rgbf; return it + 4; } if (spec.starts_with("rgb")) { color_format = ColorFormat::rgb; return it + 3; } if (spec.starts_with("hsv")) { color_format = ColorFormat::hsv; return it + 3; } throw std::format_error("Invalid format specifier for omath::Color. Use rgb, rgbf, or hsv."); } template auto format(const omath::Color& col, FormatContext& ctx) const { std::string str; switch (color_format) { case ColorFormat::rgb: str = col.to_string(); break; case ColorFormat::rgbf: str = col.to_rgbf_string(); break; case ColorFormat::hsv: str = col.to_hsv_string(); break; } if constexpr (std::is_same_v) return std::format_to(ctx.out(), "{}", str); if constexpr (std::is_same_v) return std::format_to(ctx.out(), L"{}", std::wstring(str.cbegin(), str.cend())); if constexpr (std::is_same_v) return std::format_to(ctx.out(), u8"{}", std::u8string(str.cbegin(), str.cend())); std::unreachable(); } };