diff --git a/loader/include/Geode/utils/ColorCode.hpp b/loader/include/Geode/utils/ColorCode.hpp new file mode 100644 index 000000000..7c57d4f0e --- /dev/null +++ b/loader/include/Geode/utils/ColorCode.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +namespace geode { + template + concept CanSetColor3B = requires(T object) { + object.setColor(cocos2d::ccColor3B()); + object.setOpacity(GLubyte()); + }; + template + concept CanSetColor4B = requires(T object) { object.setColor(cocos2d::ccColor4B()); }; + template + concept CanSetColor4F = requires(T object) { object.setColor(cocos2d::ccColor4F()); }; + + struct GEODE_DLL Color final { + /// Parses the given hex string using one of the following formats: + /// #R/G/B, #RR/GG/BB, #RGB, #RGBA, #RRGGBB or #RRGGBBAA + /// Where the # prefix is optional and / means it's 1 byte/nibble representing all channels + /// @returns The parsed result + static Result parse(std::string_view hex); + private: + static GLubyte getByte(uint32_t colorNum, size_t index, bool isShort); + public: + operator cocos2d::ccColor3B() const; + operator cocos2d::ccColor4B() const; + operator cocos2d::ccColor4F() const; + operator cocos2d::extension::HSV() const; + Color& operator=(Color&& other) noexcept = default; + Color& operator=(const Color& other) noexcept = default; + + GLubyte r; + GLubyte g; + GLubyte b; + GLubyte a; + + Color(Color&& other) noexcept = default; + Color(const Color& other) noexcept = default; + /// @returns The equivalent of #000000{a} + Color(GLubyte alpha = 0); + Color(GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha = 0xFF); + Color(const cocos2d::ccColor3B& color, GLubyte alpha = 0xFF); + Color(const cocos2d::ccColor4B& color); + Color(const cocos2d::ccColor4F& color); + Color(const cocos2d::extension::HSV& hsv, GLubyte alpha = 0xFF); + + void applyH(double hue); + void applyHue(double hue); + void applyS(double saturation); + void applySaturation(double saturation); + void applyV(double value); + void applyValue(double value); + void applyB(double brightness); + void applyBrightness(double brightness); + void applyHSV(const cocos2d::extension::HSV& hsv); + cocos2d::ccColor3B to3B() const; + cocos2d::ccColor4B to4B() const; + cocos2d::ccColor4F to4F() const; + cocos2d::extension::HSV toHSV() const; + /// @returns True if alpha is 0 + bool isInvisible() const; + /// @returns True if alpha is bigger than 0 and smaller than 255 + bool isTranslucent() const; + /// @returns True if alpha is 255 + bool isOpaque() const; + template requires (CanSetColor3B || CanSetColor4B || CanSetColor4F) + inline void applyTo(T* node) const { + if constexpr (CanSetColor3B) { + node->setColor(this->to3B()); + node->setOpacity(a); + } else if constexpr (CanSetColor4B) { + node->setColor(this->to4B()); + } else { + node->setColor(this->to4F()); + } + } + }; +} \ No newline at end of file diff --git a/loader/src/utils/ColorCode.cpp b/loader/src/utils/ColorCode.cpp new file mode 100644 index 000000000..c0d7a3f62 --- /dev/null +++ b/loader/src/utils/ColorCode.cpp @@ -0,0 +1,205 @@ +#include + +using namespace geode::prelude; + +Result Color::parse(std::string_view hex) { + if (hex.starts_with("#")) { + hex.remove_prefix(1); + } + + const size_t size = hex.size(); + + // If the string is an exact size of 1, 2, 3, 4, 6 or 8 + if (!size || (size > 4 && size != 6 && size != 8)) { + return Err("Unsupported size"); + } + + GEODE_UNWRAP_INTO(const uint32_t colorNum, utils::numFromString(hex, 16)); + ccColor4B color; + + if (size <= 2) { + color.r = color.g = color.b = Color::getByte(colorNum, 0, size == 1); + color.a = 0xFF; + } else if (size % 3 == 0) { + const bool isShort = size == 3; + + color.r = Color::getByte(colorNum, 2, isShort); + color.g = Color::getByte(colorNum, 1, isShort); + color.b = Color::getByte(colorNum, 0, isShort); + color.a = 0xFF; + } else { + const bool isShort = size == 4; + + color.r = Color::getByte(colorNum, 3, isShort); + color.g = Color::getByte(colorNum, 2, isShort); + color.b = Color::getByte(colorNum, 1, isShort); + color.a = Color::getByte(colorNum, 0, isShort); + } + + return Ok(Color(color)); +} + +GLubyte Color::getByte(uint32_t colorNum, size_t index, bool isShort) { + const uint32_t shiftSize = (isShort ? 4 : 8) * index; + const GLubyte byte = (colorNum >> shiftSize) & 0xFF; + + // If it's short, mirror the nibble + return isShort ? (byte << 4) | byte : byte; +} + + +Color::operator ccColor3B() const { + return { r, g, b }; +} + +Color::operator ccColor4B() const { + return { r, g, b, a }; +} + +Color::operator ccColor4F() const { + return { r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f }; +} + +Color::operator HSV() const { + const double red = r / 255.0; + const double green = g / 255.0; + const double blue = b / 255.0; + const double max = std::max({ red, green, blue }); + const double delta = max - std::min({ red, green, blue }); + HSV hsv; + + if (delta == 0) { + hsv.h = 0; + } else if (max == red) { + hsv.h = 60 * std::fmod((green - blue) / delta, 6); + } else if (max == green) { + hsv.h = 60 * ((blue - red) / delta + 2); + } else { + hsv.h = 60 * ((red - green) / delta + 4); + } + + if (hsv.h < 0) { + hsv.h += 360; + } + + hsv.s = max == 0 ? 0 : delta / max; + hsv.v = max; + + return hsv; +} + +Color::Color(GLubyte alpha): r(0x00), g(0x00), b(0x00), a(alpha) { } + +Color::Color(GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha): r(red), g(green), b(blue), a(alpha) { } + +Color::Color(const ccColor3B& color, GLubyte alpha): r(color.r), g(color.g), b(color.b), a(alpha) { } + +Color::Color(const ccColor4B& color): r(color.r), g(color.g), b(color.b), a(color.a) { } + +Color::Color(const ccColor4F& color): r(std::round(color.r * 255)), g(std::round(color.g * 255)), b(std::round(color.b * 255)), a(std::round(color.a * 255)) { } + +Color::Color(const HSV& hsv, GLubyte alpha): a(alpha) { + double correctedHue = std::fmod(hsv.h, 360); + + if (correctedHue < 0) { + correctedHue += 360; + } + + const double c = hsv.v * hsv.s; + const double x = c * (1 - std::abs(std::fmod(correctedHue / 60, 2) - 1)); + const double m = hsv.v - c; + double rp, gp, bp; + + if (correctedHue < 60) { + rp = c; gp = x; bp = 0; + } else if (correctedHue < 120) { + rp = x; gp = c; bp = 0; + } else if (correctedHue < 180) { + rp = 0; gp = c; bp = x; + } else if (correctedHue < 240) { + rp = 0; gp = x; bp = c; + } else if (correctedHue < 300) { + rp = x; gp = 0; bp = c; + } else { + rp = c; gp = 0; bp = x; + } + + r = std::round((rp + m) * 255); + g = std::round((gp + m) * 255); + b = std::round((bp + m) * 255); +} + +void Color::applyH(double hue) { + this->applyHSV({ hue, 1, 1 }); +} + +void Color::applyHue(double hue) { + this->applyHSV({ hue, 1, 1 }); +} + +void Color::applyS(double saturation) { + this->applyHSV({ 0, saturation, 1 }); +} + +void Color::applySaturation(double saturation) { + this->applyHSV({ 0, saturation, 1 }); +} + +void Color::applyV(double value) { + this->applyHSV({ 0, 1, value }); +} + +void Color::applyValue(double value) { + this->applyHSV({ 0, 1, value }); +} + +void Color::applyB(double brightness) { + this->applyHSV({ 0, 1, brightness }); +} + +void Color::applyBrightness(double brightness) { + this->applyHSV({ 0, 1, brightness }); +} + +void Color::applyHSV(const HSV& hsv) { + HSV currentHSV = *this; + + currentHSV.h = std::fmod(currentHSV.h + hsv.h, 360); + + if (currentHSV.h < 0) { + currentHSV.h += 360; + } + + currentHSV.s = std::clamp(currentHSV.s * hsv.s, 0, 1); + currentHSV.v = std::clamp(currentHSV.v * hsv.v, 0, 1); + + *this = currentHSV; +} + +ccColor3B Color::to3B() const { + return *this; +} + +ccColor4B Color::to4B() const { + return *this; +} + +ccColor4F Color::to4F() const { + return *this; +} + +HSV Color::toHSV() const { + return *this; +} + +bool Color::isInvisible() const { + return a == 0x00; +} + +bool Color::isTranslucent() const { + return a > 0x00 && a < 0xFF; +} + +bool Color::isOpaque() const { + return a == 0xFF; +} \ No newline at end of file