From 350dd93b376230a526e8ff558899333751daca91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Thu, 16 Oct 2025 23:36:42 +0200 Subject: [PATCH 1/2] Move SVG color to a separate SVGBrush struct --- .../reanimated/CSS/InterpolatorRegistry.cpp | 11 +- .../reanimated/CSS/common/values/CSSColor.cpp | 160 ++++++++---------- .../reanimated/CSS/common/values/CSSColor.h | 52 +++--- .../CSS/common/values/CSSValueVariant.cpp | 2 + .../values/SimpleValueInterpolator.cpp | 2 + .../reanimated/CSS/svg/values/SVGBrush.cpp | 85 ++++++++++ .../cpp/reanimated/CSS/svg/values/SVGBrush.h | 38 +++++ 7 files changed, 229 insertions(+), 121 deletions(-) create mode 100644 packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.cpp create mode 100644 packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpp index 2626b3dc9b3a..fe2b5b47929e 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -31,8 +32,8 @@ namespace reanimated::css { namespace { // Private implementation details -const auto BLACK = CSSColor(0, 0, 0, 255); -const auto TRANSPARENT = CSSColor::Transparent; +const std::array BLACK = {0, 0, 0, 255}; +const std::array TRANSPARENT = {0, 0, 0, 0}; InterpolatorFactoriesRecord mergeInterpolators( const std::vector &maps) { @@ -275,17 +276,17 @@ const InterpolatorFactoriesRecord IMAGE_INTERPOLATORS = mergeInterpolators( // ================= const InterpolatorFactoriesRecord SVG_COLOR_INTERPOLATORS = { - {"color", value(BLACK)}, + {"color", value(BLACK)}, }; const InterpolatorFactoriesRecord SVG_FILL_INTERPOLATORS = { - {"fill", value(BLACK)}, + {"fill", value(BLACK)}, {"fillOpacity", value(1)}, {"fillRule", value(0)}, }; const InterpolatorFactoriesRecord SVG_STROKE_INTERPOLATORS = { - {"stroke", value(BLACK)}, + {"stroke", value(BLACK)}, {"strokeWidth", value(1)}, {"strokeOpacity", value(1)}, {"strokeDasharray", diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.cpp index ef54073bd344..e780db8f8ed5 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.cpp @@ -1,15 +1,23 @@ #include +#include + +#include namespace reanimated::css { -CSSColor::CSSColor() - : channels{0, 0, 0, 0}, colorType(ColorType::Transparent) {} +// CSSColorBase template implementations + +template +CSSColorBase::CSSColorBase() + : channels{0, 0, 0, 0}, colorType(TColorType::Transparent) {} -CSSColor::CSSColor(ColorType colorType) +template +CSSColorBase::CSSColorBase(TColorType colorType) : channels{0, 0, 0, 0}, colorType(colorType) {} -CSSColor::CSSColor(int64_t numberValue) - : channels{0, 0, 0, 0}, colorType(ColorType::Rgba) { +template +CSSColorBase::CSSColorBase(int64_t numberValue) + : channels{0, 0, 0, 0}, colorType(TColorType::Rgba) { uint32_t color; // On Android, colors are represented as signed 32-bit integers. In JS, we use // a bitwise operation (normalizedColor = normalizedColor | 0x0) to ensure the @@ -26,130 +34,98 @@ CSSColor::CSSColor(int64_t numberValue) channels[1] = (color >> 8) & 0xFF; // Green channels[2] = color & 0xFF; // Blue channels[3] = (color >> 24) & 0xFF; // Alpha - colorType = ColorType::Rgba; -} - -CSSColor::CSSColor(const std::string &colorString) - : channels{0, 0, 0, 0}, colorType(ColorType::Transparent) { - if (colorString == "transparent") { - colorType = ColorType::Transparent; - } else if (colorString == "currentColor") { - colorType = ColorType::CurrentColor; - } else { - throw std::invalid_argument( - "[Reanimated] CSSColor: Invalid string value: " + colorString); - } + colorType = TColorType::Rgba; } -CSSColor::CSSColor(const uint8_t r, const uint8_t g, const uint8_t b) - : channels{r, g, b, 255}, colorType(ColorType::Rgba) {} - -CSSColor::CSSColor( +template +CSSColorBase::CSSColorBase( const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) - : channels{r, g, b, a}, colorType(ColorType::Rgba) {} + : channels{r, g, b, a}, colorType(TColorType::Rgba) {} + +template +CSSColorBase::CSSColorBase(ColorChannels colorChannels) + : channels{std::move(colorChannels)}, colorType(TColorType::Rgba) {} + +template +TDerived CSSColorBase::interpolate( + double progress, + const TDerived &to) const { + ColorChannels fromChannels = channels; + ColorChannels toChannels = to.channels; + + if (colorType == TColorType::Transparent) { + fromChannels = {toChannels[0], toChannels[1], toChannels[2], 0}; + } else if (to.colorType == TColorType::Transparent) { + toChannels = {fromChannels[0], fromChannels[1], fromChannels[2], 0}; + } + + ColorChannels resultChannels; + for (size_t i = 0; i < 4; ++i) { + const auto &from = fromChannels[i]; + const auto &to = toChannels[i]; + // Cast one of operands to double to avoid unsigned int subtraction overflow + // (when from > to) + const double interpolated = + (static_cast(to) - from) * progress + from; + resultChannels[i] = + static_cast(std::round(std::clamp(interpolated, 0.0, 255.0))); + } + + return TDerived(std::move(resultChannels)); +} + +template +bool CSSColorBase::operator==( + const TDerived &other) const { + return colorType == other.colorType && channels == other.channels; +} -CSSColor::CSSColor(const ColorChannels &colorChannels) - : channels{colorChannels[0], colorChannels[1], colorChannels[2], colorChannels[3]}, - colorType(ColorType::Rgba) {} +// CSSColor implementations CSSColor::CSSColor(jsi::Runtime &rt, const jsi::Value &jsiValue) - : channels{0, 0, 0, 0}, colorType(ColorType::Transparent) { + : CSSColorBase(CSSColorType::Transparent) { if (jsiValue.isNumber()) { *this = CSSColor(jsiValue.getNumber()); - } else if (jsiValue.isString()) { - *this = CSSColor(jsiValue.getString(rt).utf8(rt)); - } else { - *this = Transparent; } } CSSColor::CSSColor(const folly::dynamic &value) - : channels{0, 0, 0, 0}, colorType(ColorType::Transparent) { + : CSSColorBase(CSSColorType::Transparent) { if (value.isNumber()) { *this = CSSColor(value.asInt()); - } else if (value.isString()) { - *this = CSSColor(value.getString()); - } else { - *this = Transparent; } } bool CSSColor::canConstruct(jsi::Runtime &rt, const jsi::Value &jsiValue) { return jsiValue.isNumber() || jsiValue.isUndefined() || - (jsiValue.isString() && - isValidColorString(jsiValue.getString(rt).utf8(rt))); + (jsiValue.isString() && jsiValue.getString(rt).utf8(rt) == "transparent"); } bool CSSColor::canConstruct(const folly::dynamic &value) { return value.isNumber() || value.empty() || - (value.isString() && isValidColorString(value.getString())); + (value.isString() && value.getString() == "transparent"); } folly::dynamic CSSColor::toDynamic() const { - if (colorType == ColorType::Transparent) { - return 0x00000000; + if (colorType == CSSColorType::Rgba) { + return (channels[3] << 24) | (channels[0] << 16) | (channels[1] << 8) | + channels[2]; } - return (channels[3] << 24) | (channels[0] << 16) | (channels[1] << 8) | - channels[2]; + return 0; } std::string CSSColor::toString() const { - if (colorType == ColorType::Rgba) { + if (colorType == CSSColorType::Rgba) { return "rgba(" + std::to_string(channels[0]) + "," + std::to_string(channels[1]) + "," + std::to_string(channels[2]) + "," + std::to_string(channels[3]) + ")"; } - if (colorType == ColorType::CurrentColor) { - return "currentColor"; - } return "transparent"; } -CSSColor CSSColor::interpolate(const double progress, const CSSColor &to) - const { - if ((to.colorType == ColorType::Transparent && - colorType == ColorType::Transparent) || - colorType == ColorType::CurrentColor || - to.colorType == ColorType::CurrentColor) { - return progress < 0.5 ? *this : to; - } - - ColorChannels fromChannels = channels; - ColorChannels toChannels = to.channels; - if (colorType == ColorType::Transparent) { - fromChannels = {toChannels[0], toChannels[1], toChannels[2], 0}; - } else if (to.colorType == ColorType::Transparent) { - toChannels = {fromChannels[0], fromChannels[1], fromChannels[2], 0}; - } - - ColorChannels resultChannels; - for (size_t i = 0; i < 4; ++i) { - resultChannels[i] = - interpolateChannel(fromChannels[i], toChannels[i], progress); - } - - return CSSColor(resultChannels); -} - -uint8_t CSSColor::interpolateChannel( - const uint8_t from, - const uint8_t to, - const double progress) { - // Cast one of operands to double to avoid unsigned int subtraction overflow - // (when from > to) - double interpolated = (static_cast(to) - from) * progress + from; - return static_cast(std::round(std::clamp(interpolated, 0.0, 255.0))); -} - -bool CSSColor::operator==(const CSSColor &other) const { - return colorType == other.colorType && channels[0] == other.channels[0] && - channels[1] == other.channels[1] && channels[2] == other.channels[2] && - channels[3] == other.channels[3]; -} - #ifndef NDEBUG std::ostream &operator<<(std::ostream &os, const CSSColor &colorValue) { @@ -159,8 +135,8 @@ std::ostream &operator<<(std::ostream &os, const CSSColor &colorValue) { #endif // NDEBUG -bool CSSColor::isValidColorString(const std::string &colorString) { - return colorString == "transparent" || colorString == "currentColor"; -} +// Explicit template instantiation +template struct CSSColorBase; +template struct CSSColorBase; } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h index 6f5a08e8875d..78a0f686cfc6 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h @@ -7,26 +7,40 @@ namespace reanimated::css { -enum class ColorType { - Rgba, - Transparent, - CurrentColor, // for SVG +// Base class with common color value functionality + +template +concept ColorTypeEnum = std::is_enum_v && requires { + T::Rgba; + T::Transparent; }; -struct CSSColor : public CSSSimpleValue { +template +struct CSSColorBase : public CSSSimpleValue { ColorChannels channels; - ColorType colorType; + TColorType colorType; + + CSSColorBase(); + explicit CSSColorBase(TColorType colorType); + explicit CSSColorBase(int64_t numberValue); - static const CSSColor Transparent; + explicit CSSColorBase(uint8_t r, uint8_t g, uint8_t b, uint8_t a); + explicit CSSColorBase(ColorChannels colorChannels); - CSSColor(); - explicit CSSColor(ColorType colorType); - explicit CSSColor(int64_t numberValue); - explicit CSSColor(const std::string &colorString); + TDerived interpolate(double progress, const TDerived &to) const override; - explicit CSSColor(uint8_t r, uint8_t g, uint8_t b); - explicit CSSColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a); - explicit CSSColor(const ColorChannels &colorChannels); + bool operator==(const TDerived &other) const; +}; + +// Enum class for color type + +enum class CSSColorType { + Rgba, + Transparent, +}; + +struct CSSColor : public CSSColorBase { + using CSSColorBase::CSSColorBase; explicit CSSColor(jsi::Runtime &rt, const jsi::Value &jsiValue); explicit CSSColor(const folly::dynamic &value); @@ -36,20 +50,10 @@ struct CSSColor : public CSSSimpleValue { folly::dynamic toDynamic() const override; std::string toString() const override; - CSSColor interpolate(double progress, const CSSColor &to) const override; - - static uint8_t interpolateChannel(uint8_t from, uint8_t to, double progress); - - bool operator==(const CSSColor &other) const; #ifndef NDEBUG friend std::ostream &operator<<(std::ostream &os, const CSSColor &colorValue); #endif // NDEBUG - - private: - static bool isValidColorString(const std::string &colorString); }; -inline const CSSColor CSSColor::Transparent(ColorType::Transparent); - } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpp index f6b6c66d95a6..7a7552892c44 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -179,5 +180,6 @@ template class CSSValueVariant>; template class CSSValueVariant; template class CSSValueVariant; template class CSSValueVariant; +template class CSSValueVariant; } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpp index aa4824771ef0..0f72ab9e9334 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -66,5 +67,6 @@ template class SimpleValueInterpolator>; template class SimpleValueInterpolator; template class SimpleValueInterpolator; template class SimpleValueInterpolator; +template class SimpleValueInterpolator; } // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.cpp new file mode 100644 index 000000000000..a5d374aa155e --- /dev/null +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.cpp @@ -0,0 +1,85 @@ +#include + +namespace reanimated::css { + +SVGBrush::SVGBrush(jsi::Runtime &rt, const jsi::Value &value) + : CSSColorBase(SVGBrushType::Transparent) { + if (value.isNumber()) { + *this = SVGBrush(value.getNumber()); + } else if ( + value.isString() && value.getString(rt).utf8(rt) == "currentColor") { + colorType = SVGBrushType::CurrentColor; + } +} + +SVGBrush::SVGBrush(const folly::dynamic &value) + : CSSColorBase(SVGBrushType::Transparent) { + if (value.isNumber()) { + *this = SVGBrush(value.getDouble()); + } else if (value.isString() && value.getString() == "currentColor") { + colorType = SVGBrushType::CurrentColor; + } +} + +bool SVGBrush::canConstruct(jsi::Runtime &rt, const jsi::Value &jsiValue) { + return jsiValue.isNumber() || jsiValue.isUndefined() || + (jsiValue.isString() && + isValidColorString(jsiValue.getString(rt).utf8(rt))); +} + +bool SVGBrush::canConstruct(const folly::dynamic &value) { + return value.isNumber() || value.empty() || + isValidColorString(value.asString()); +} + +folly::dynamic SVGBrush::toDynamic() const { + switch (colorType) { + case SVGBrushType::Rgba: + return (channels[3] << 24) | (channels[0] << 16) | (channels[1] << 8) | + channels[2]; + case SVGBrushType::CurrentColor: + return nullptr; // currentColor is represented as nullptr in SVG + default: + return 0; // Transparent + } +} + +std::string SVGBrush::toString() const { + switch (colorType) { + case SVGBrushType::Rgba: + return "rgba(" + std::to_string(channels[0]) + "," + + std::to_string(channels[1]) + "," + std::to_string(channels[2]) + + "," + std::to_string(channels[3]) + ")"; + case SVGBrushType::CurrentColor: + return "currentColor"; + default: + return "transparent"; + } +} + +SVGBrush SVGBrush::interpolate(double progress, const SVGBrush &to) const { + if (!isInterpolatable()) { + return progress < 0.5 ? *this : to; + } + return CSSColorBase::interpolate(progress, to); +} + +bool SVGBrush::isInterpolatable() const { + return colorType == SVGBrushType::Rgba || + colorType == SVGBrushType::Transparent; +} + +#ifndef NDEBUG + +std::ostream &operator<<(std::ostream &os, const SVGBrush &colorValue) { + os << "SVGBrush(" << colorValue.toString() << ")"; + return os; +} + +#endif // NDEBUG + +bool SVGBrush::isValidColorString(const std::string &value) { + return value == "transparent" || value == "currentColor"; +} + +} // namespace reanimated::css diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h new file mode 100644 index 000000000000..c38094f59482 --- /dev/null +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +namespace reanimated::css { + +enum class SVGBrushType { + Rgba, + Transparent, + CurrentColor, +}; + +struct SVGBrush : public CSSColorBase { + using CSSColorBase::CSSColorBase; + + explicit SVGBrush(jsi::Runtime &rt, const jsi::Value &jsiValue); + explicit SVGBrush(const folly::dynamic &value); + + static bool canConstruct(jsi::Runtime &rt, const jsi::Value &jsiValue); + static bool canConstruct(const folly::dynamic &value); + + folly::dynamic toDynamic() const override; + std::string toString() const override; + + SVGBrush interpolate(double progress, const SVGBrush &to) const override; + bool isInterpolatable() const; + +#ifndef NDEBUG + friend std::ostream &operator<<(std::ostream &os, const SVGBrush &colorValue); +#endif // NDEBUG + + private: + static bool isValidColorString(const std::string &value); +}; + +} // namespace reanimated::css From 4ed7eccc8fd6fb40d253ec25294015096865819a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 17 Oct 2025 15:22:12 +0200 Subject: [PATCH 2/2] Attempt to fix CI failure --- .../Common/cpp/reanimated/CSS/common/values/CSSColor.h | 1 + .../Common/cpp/reanimated/CSS/svg/values/SVGBrush.h | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h index 78a0f686cfc6..045605560c52 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSColor.h @@ -41,6 +41,7 @@ enum class CSSColorType { struct CSSColor : public CSSColorBase { using CSSColorBase::CSSColorBase; + using CSSColorBase::operator==; explicit CSSColor(jsi::Runtime &rt, const jsi::Value &jsiValue); explicit CSSColor(const folly::dynamic &value); diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h index c38094f59482..2bd6693c425d 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/values/SVGBrush.h @@ -14,6 +14,7 @@ enum class SVGBrushType { struct SVGBrush : public CSSColorBase { using CSSColorBase::CSSColorBase; + using CSSColorBase::operator==; explicit SVGBrush(jsi::Runtime &rt, const jsi::Value &jsiValue); explicit SVGBrush(const folly::dynamic &value);