diff --git a/include/omath/utility/color.hpp b/include/omath/utility/color.hpp index a1a3c437..0f848eee 100644 --- a/include/omath/utility/color.hpp +++ b/include/omath/utility/color.hpp @@ -16,19 +16,28 @@ namespace omath float value{}; }; - class Color final : public Vector4 + class Color final { + Vector4 m_value; public: - constexpr Color(const float r, const float g, const float b, const float a) noexcept: Vector4(r, g, b, a) + constexpr const Vector4& value() const { - clamp(0.f, 1.f); + 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}; + return Color(Vector4(r, g, b, a) / 255.f); } [[nodiscard]] @@ -82,9 +91,9 @@ namespace omath { Hsv hsv_data; - const float& red = x; - const float& green = y; - const float& blue = z; + 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}); @@ -109,11 +118,6 @@ namespace omath return hsv_data; } - - constexpr explicit Color(const Vector4& vec) noexcept: Vector4(vec) - { - clamp(0.f, 1.f); - } constexpr void set_hue(const float hue) noexcept { auto hsv = to_hsv(); @@ -141,7 +145,7 @@ namespace omath constexpr Color blend(const Color& other, float ratio) const noexcept { ratio = std::clamp(ratio, 0.f, 1.f); - return Color(*this * (1.f - ratio) + other * ratio); + return Color(this->m_value * (1.f - ratio) + other.m_value * ratio); } [[nodiscard]] static constexpr Color red() @@ -160,16 +164,26 @@ namespace omath [[nodiscard]] ImColor to_im_color() const noexcept { - return {to_im_vec4()}; + return {m_value.to_im_vec4()}; } #endif [[nodiscard]] std::string to_string() const noexcept { return std::format("[r:{}, g:{}, b:{}, a:{}]", - static_cast(x * 255.f), - static_cast(y * 255.f), - static_cast(z * 255.f), - static_cast(w * 255.f)); + 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 { @@ -188,23 +202,55 @@ namespace omath template<> struct std::formatter // NOLINT(*-dcl58-cpp) { - [[nodiscard]] - static constexpr auto parse(const std::format_parse_context& ctx) + enum class ColorFormat { rgb, rgbf, hsv }; + ColorFormat color_format = ColorFormat::rgb; + + constexpr auto parse(std::format_parse_context& ctx) { - return ctx.begin(); + 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 - [[nodiscard]] - static auto format(const omath::Color& col, FormatContext& ctx) + 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(), "{}", col.to_string()); + return std::format_to(ctx.out(), "{}", str); if constexpr (std::is_same_v) - return std::format_to(ctx.out(), L"{}", col.to_wstring()); - + 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"{}", col.to_u8string()); + return std::format_to(ctx.out(), u8"{}", std::u8string(str.cbegin(), str.cend())); std::unreachable(); } diff --git a/tests/general/unit_test_color_grouped.cpp b/tests/general/unit_test_color_grouped.cpp index a32eaf1a..62537076 100644 --- a/tests/general/unit_test_color_grouped.cpp +++ b/tests/general/unit_test_color_grouped.cpp @@ -26,38 +26,38 @@ class UnitTestColorGrouped : public ::testing::Test TEST_F(UnitTestColorGrouped, Constructor_Float) { constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f); - EXPECT_FLOAT_EQ(color.x, 0.5f); - EXPECT_FLOAT_EQ(color.y, 0.5f); - EXPECT_FLOAT_EQ(color.z, 0.5f); - EXPECT_FLOAT_EQ(color.w, 1.0f); + EXPECT_FLOAT_EQ(color.value().x, 0.5f); + EXPECT_FLOAT_EQ(color.value().y, 0.5f); + EXPECT_FLOAT_EQ(color.value().z, 0.5f); + EXPECT_FLOAT_EQ(color.value().w, 1.0f); } TEST_F(UnitTestColorGrouped, Constructor_Vector4) { constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f); constexpr Color color(vec); - EXPECT_FLOAT_EQ(color.x, 0.2f); - EXPECT_FLOAT_EQ(color.y, 0.4f); - EXPECT_FLOAT_EQ(color.z, 0.6f); - EXPECT_FLOAT_EQ(color.w, 0.8f); + EXPECT_FLOAT_EQ(color.value().x, 0.2f); + EXPECT_FLOAT_EQ(color.value().y, 0.4f); + EXPECT_FLOAT_EQ(color.value().z, 0.6f); + EXPECT_FLOAT_EQ(color.value().w, 0.8f); } TEST_F(UnitTestColorGrouped, FromRGBA) { constexpr Color color = Color::from_rgba(128, 64, 32, 255); - EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f); - EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f); - EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f); - EXPECT_FLOAT_EQ(color.w, 1.0f); + EXPECT_FLOAT_EQ(color.value().x, 128.0f / 255.0f); + EXPECT_FLOAT_EQ(color.value().y, 64.0f / 255.0f); + EXPECT_FLOAT_EQ(color.value().z, 32.0f / 255.0f); + EXPECT_FLOAT_EQ(color.value().w, 1.0f); } TEST_F(UnitTestColorGrouped, FromHSV) { constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV - EXPECT_FLOAT_EQ(color.x, 1.0f); - EXPECT_FLOAT_EQ(color.y, 0.0f); - EXPECT_FLOAT_EQ(color.z, 0.0f); - EXPECT_FLOAT_EQ(color.w, 1.0f); + EXPECT_FLOAT_EQ(color.value().x, 1.0f); + EXPECT_FLOAT_EQ(color.value().y, 0.0f); + EXPECT_FLOAT_EQ(color.value().z, 0.0f); + EXPECT_FLOAT_EQ(color.value().w, 1.0f); } TEST_F(UnitTestColorGrouped, ToHSV) @@ -71,10 +71,10 @@ TEST_F(UnitTestColorGrouped, ToHSV) TEST_F(UnitTestColorGrouped, Blend) { const Color blended = color1.blend(color2, 0.5f); - EXPECT_FLOAT_EQ(blended.x, 0.5f); - EXPECT_FLOAT_EQ(blended.y, 0.5f); - EXPECT_FLOAT_EQ(blended.z, 0.0f); - EXPECT_FLOAT_EQ(blended.w, 1.0f); + EXPECT_FLOAT_EQ(blended.value().x, 0.5f); + EXPECT_FLOAT_EQ(blended.value().y, 0.5f); + EXPECT_FLOAT_EQ(blended.value().z, 0.0f); + EXPECT_FLOAT_EQ(blended.value().w, 1.0f); } TEST_F(UnitTestColorGrouped, PredefinedColors) @@ -83,20 +83,20 @@ TEST_F(UnitTestColorGrouped, PredefinedColors) constexpr Color green = Color::green(); constexpr Color blue = Color::blue(); - EXPECT_FLOAT_EQ(red.x, 1.0f); - EXPECT_FLOAT_EQ(red.y, 0.0f); - EXPECT_FLOAT_EQ(red.z, 0.0f); - EXPECT_FLOAT_EQ(red.w, 1.0f); + EXPECT_FLOAT_EQ(red.value().x, 1.0f); + EXPECT_FLOAT_EQ(red.value().y, 0.0f); + EXPECT_FLOAT_EQ(red.value().z, 0.0f); + EXPECT_FLOAT_EQ(red.value().w, 1.0f); - EXPECT_FLOAT_EQ(green.x, 0.0f); - EXPECT_FLOAT_EQ(green.y, 1.0f); - EXPECT_FLOAT_EQ(green.z, 0.0f); - EXPECT_FLOAT_EQ(green.w, 1.0f); + EXPECT_FLOAT_EQ(green.value().x, 0.0f); + EXPECT_FLOAT_EQ(green.value().y, 1.0f); + EXPECT_FLOAT_EQ(green.value().z, 0.0f); + EXPECT_FLOAT_EQ(green.value().w, 1.0f); - EXPECT_FLOAT_EQ(blue.x, 0.0f); - EXPECT_FLOAT_EQ(blue.y, 0.0f); - EXPECT_FLOAT_EQ(blue.z, 1.0f); - EXPECT_FLOAT_EQ(blue.w, 1.0f); + EXPECT_FLOAT_EQ(blue.value().x, 0.0f); + EXPECT_FLOAT_EQ(blue.value().y, 0.0f); + EXPECT_FLOAT_EQ(blue.value().z, 1.0f); + EXPECT_FLOAT_EQ(blue.value().w, 1.0f); } TEST_F(UnitTestColorGrouped, BlendVector3) @@ -104,9 +104,9 @@ TEST_F(UnitTestColorGrouped, BlendVector3) constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green constexpr Color blended = v1.blend(v2, 0.5f); - EXPECT_FLOAT_EQ(blended.x, 0.5f); - EXPECT_FLOAT_EQ(blended.y, 0.5f); - EXPECT_FLOAT_EQ(blended.z, 0.0f); + EXPECT_FLOAT_EQ(blended.value().x, 0.5f); + EXPECT_FLOAT_EQ(blended.value().y, 0.5f); + EXPECT_FLOAT_EQ(blended.value().z, 0.0f); } // From unit_test_color_extra.cpp @@ -148,37 +148,37 @@ TEST(UnitTestColorGrouped_Extra, BlendEdgeCases) constexpr Color a = Color::red(); constexpr Color b = Color::blue(); constexpr auto r0 = a.blend(b, 0.f); - EXPECT_FLOAT_EQ(r0.x, a.x); + EXPECT_FLOAT_EQ(r0.value().x, a.value().x); constexpr auto r1 = a.blend(b, 1.f); - EXPECT_FLOAT_EQ(r1.x, b.x); + EXPECT_FLOAT_EQ(r1.value().x, b.value().x); } // From unit_test_color_more.cpp TEST(UnitTestColorGrouped_More, DefaultCtorIsZero) { constexpr Color c; - EXPECT_FLOAT_EQ(c.x, 0.0f); - EXPECT_FLOAT_EQ(c.y, 0.0f); - EXPECT_FLOAT_EQ(c.z, 0.0f); - EXPECT_FLOAT_EQ(c.w, 0.0f); + EXPECT_FLOAT_EQ(c.value().x, 0.0f); + EXPECT_FLOAT_EQ(c.value().y, 0.0f); + EXPECT_FLOAT_EQ(c.value().z, 0.0f); + EXPECT_FLOAT_EQ(c.value().w, 0.0f); } TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB) { constexpr Color c(1.2f, -0.5f, 0.5f, 2.0f); - EXPECT_FLOAT_EQ(c.x, 1.0f); - EXPECT_FLOAT_EQ(c.y, 0.0f); - EXPECT_FLOAT_EQ(c.z, 0.5f); - EXPECT_FLOAT_EQ(c.w, 2.0f); + EXPECT_FLOAT_EQ(c.value().x, 1.0f); + EXPECT_FLOAT_EQ(c.value().y, 0.0f); + EXPECT_FLOAT_EQ(c.value().z, 0.5f); + EXPECT_FLOAT_EQ(c.value().w, 2.0f); } TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents) { constexpr Color c = Color::from_rgba(25u, 128u, 230u, 64u); - EXPECT_NEAR(c.x, 25.0f/255.0f, 1e-6f); - EXPECT_NEAR(c.y, 128.0f/255.0f, 1e-6f); - EXPECT_NEAR(c.z, 230.0f/255.0f, 1e-6f); - EXPECT_NEAR(c.w, 64.0f/255.0f, 1e-6f); + EXPECT_NEAR(c.value().x, 25.0f/255.0f, 1e-6f); + EXPECT_NEAR(c.value().y, 128.0f/255.0f, 1e-6f); + EXPECT_NEAR(c.value().z, 230.0f/255.0f, 1e-6f); + EXPECT_NEAR(c.value().w, 64.0f/255.0f, 1e-6f); } TEST(UnitTestColorGrouped_More, BlendProducesIntermediate) @@ -186,10 +186,10 @@ TEST(UnitTestColorGrouped_More, BlendProducesIntermediate) constexpr Color c0(0.0f, 0.0f, 0.0f, 1.0f); constexpr Color c1(1.0f, 1.0f, 1.0f, 0.0f); constexpr Color mid = c0.blend(c1, 0.5f); - EXPECT_FLOAT_EQ(mid.x, 0.5f); - EXPECT_FLOAT_EQ(mid.y, 0.5f); - EXPECT_FLOAT_EQ(mid.z, 0.5f); - EXPECT_FLOAT_EQ(mid.w, 0.5f); + EXPECT_FLOAT_EQ(mid.value().x, 0.5f); + EXPECT_FLOAT_EQ(mid.value().y, 0.5f); + EXPECT_FLOAT_EQ(mid.value().z, 0.5f); + EXPECT_FLOAT_EQ(mid.value().w, 0.5f); } TEST(UnitTestColorGrouped_More, HsvRoundTrip) @@ -197,9 +197,9 @@ TEST(UnitTestColorGrouped_More, HsvRoundTrip) constexpr Color red = Color::red(); const auto hsv = red.to_hsv(); const Color back = Color::from_hsv(hsv); - EXPECT_NEAR(back.x, 1.0f, 1e-6f); - EXPECT_NEAR(back.y, 0.0f, 1e-6f); - EXPECT_NEAR(back.z, 0.0f, 1e-6f); + EXPECT_NEAR(back.value().x, 1.0f, 1e-6f); + EXPECT_NEAR(back.value().y, 0.0f, 1e-6f); + EXPECT_NEAR(back.value().z, 0.0f, 1e-6f); } TEST(UnitTestColorGrouped_More, ToStringContainsComponents) @@ -230,18 +230,18 @@ TEST(UnitTestColorGrouped_More2, FromHsvCases) auto check_hue = [&](float h) { SCOPED_TRACE(::testing::Message() << "h=" << h); Color c = Color::from_hsv(h, 1.f, 1.f); - EXPECT_TRUE(std::isfinite(c.x)); - EXPECT_TRUE(std::isfinite(c.y)); - EXPECT_TRUE(std::isfinite(c.z)); - EXPECT_GE(c.x, -eps); - EXPECT_LE(c.x, 1.f + eps); - EXPECT_GE(c.y, -eps); - EXPECT_LE(c.y, 1.f + eps); - EXPECT_GE(c.z, -eps); - EXPECT_LE(c.z, 1.f + eps); - - float mx = std::max({c.x, c.y, c.z}); - float mn = std::min({c.x, c.y, c.z}); + EXPECT_TRUE(std::isfinite(c.value().x)); + EXPECT_TRUE(std::isfinite(c.value().y)); + EXPECT_TRUE(std::isfinite(c.value().z)); + EXPECT_GE(c.value().x, -eps); + EXPECT_LE(c.value().x, 1.f + eps); + EXPECT_GE(c.value().y, -eps); + EXPECT_LE(c.value().y, 1.f + eps); + EXPECT_GE(c.value().z, -eps); + EXPECT_LE(c.value().z, 1.f + eps); + + float mx = std::max({c.value().x, c.value().y, c.value().z}); + float mn = std::min({c.value().x, c.value().y, c.value().z}); EXPECT_GE(mx, 0.999f); EXPECT_LE(mn, 1e-3f + 1e-4f); }; @@ -261,13 +261,13 @@ TEST(UnitTestColorGrouped_More2, ToHsvAndSetters) EXPECT_NEAR(hsv.value, 0.6f, 1e-6f); c.set_hue(0.0f); - EXPECT_TRUE(std::isfinite(c.x)); + EXPECT_TRUE(std::isfinite(c.value().x)); c.set_saturation(0.0f); - EXPECT_TRUE(std::isfinite(c.y)); + EXPECT_TRUE(std::isfinite(c.value().y)); c.set_value(0.5f); - EXPECT_TRUE(std::isfinite(c.z)); + EXPECT_TRUE(std::isfinite(c.value().z)); } TEST(UnitTestColorGrouped_More2, BlendAndStaticColors) @@ -275,14 +275,14 @@ TEST(UnitTestColorGrouped_More2, BlendAndStaticColors) constexpr Color a = Color::red(); constexpr Color b = Color::blue(); constexpr auto mid = a.blend(b, 0.5f); - EXPECT_GT(mid.x, 0.f); - EXPECT_GT(mid.z, 0.f); + EXPECT_GT(mid.value().x, 0.f); + EXPECT_GT(mid.value().z, 0.f); constexpr auto all_a = a.blend(b, -1.f); - EXPECT_NEAR(all_a.x, a.x, 1e-6f); + EXPECT_NEAR(all_a.value().x, a.value().x, 1e-6f); constexpr auto all_b = a.blend(b, 2.f); - EXPECT_NEAR(all_b.z, b.z, 1e-6f); + EXPECT_NEAR(all_b.value().z, b.value().z, 1e-6f); } TEST(UnitTestColorGrouped_More2, FormatterUsesToString) @@ -291,3 +291,35 @@ TEST(UnitTestColorGrouped_More2, FormatterUsesToString) const auto formatted = std::format("{}", c); EXPECT_NE(formatted.find("r:10"), std::string::npos); } + +TEST(UnitTestColorGrouped_More2, FormatterRgb) +{ + constexpr Color c = Color::from_rgba(255, 128, 0, 64); + const auto s = std::format("{:rgb}", c); + EXPECT_NE(s.find("r:255"), std::string::npos); + EXPECT_NE(s.find("g:128"), std::string::npos); + EXPECT_NE(s.find("b:0"), std::string::npos); + EXPECT_NE(s.find("a:64"), std::string::npos); +} + +TEST(UnitTestColorGrouped_More2, FormatterRgbf) +{ + constexpr Color c(0.5f, 0.25f, 1.0f, 0.75f); + const auto s = std::format("{:rgbf}", c); + EXPECT_NE(s.find("r:"), std::string::npos); + EXPECT_NE(s.find("g:"), std::string::npos); + EXPECT_NE(s.find("b:"), std::string::npos); + EXPECT_NE(s.find("a:"), std::string::npos); + // Values should be in [0,1] float range, not 0-255 + EXPECT_EQ(s.find("r:127"), std::string::npos); + EXPECT_EQ(s.find("r:255"), std::string::npos); +} + +TEST(UnitTestColorGrouped_More2, FormatterHsv) +{ + const Color c = Color::red(); + const auto s = std::format("{:hsv}", c); + EXPECT_NE(s.find("h:"), std::string::npos); + EXPECT_NE(s.find("s:"), std::string::npos); + EXPECT_NE(s.find("v:"), std::string::npos); +}