diff --git a/change/react-native-windows-c6908db9-21b8-4875-8d44-30c63ff13f74.json b/change/react-native-windows-c6908db9-21b8-4875-8d44-30c63ff13f74.json
new file mode 100644
index 00000000000..d5fc0970c8e
--- /dev/null
+++ b/change/react-native-windows-c6908db9-21b8-4875-8d44-30c63ff13f74.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Adds color animated nodes to tick-driven animations",
+ "packageName": "react-native-windows",
+ "email": "ericroz@meta.com",
+ "dependentChangeType": "patch"
+}
diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
index 932722fbf5f..f07b7927dbd 100644
--- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
+++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
@@ -233,6 +233,7 @@
+
@@ -437,6 +438,7 @@
+
diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
index ad0c2b22282..831b3467a4c 100644
--- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
+++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
@@ -39,6 +39,9 @@
Modules\Animated
+
+ Modules\Animated
+
Modules\Animated
@@ -383,6 +386,9 @@
Modules\Animated
+
+ Modules\Animated
+
Modules\Animated
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNodeType.h b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNodeType.h
index c33141874b1..c594aeebcc8 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNodeType.h
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimatedNodeType.h
@@ -18,6 +18,7 @@ enum class AnimatedNodeType {
Diffclamp,
Transform,
Tracking,
+ Color,
};
static AnimatedNodeType AnimatedNodeTypeFromString(const std::string &string) {
@@ -43,7 +44,8 @@ static AnimatedNodeType AnimatedNodeTypeFromString(const std::string &string) {
return AnimatedNodeType::Diffclamp;
if (string == "transform")
return AnimatedNodeType::Transform;
-
- assert(string == "tracking");
- return AnimatedNodeType::Tracking;
+ if (string == "tracking")
+ return AnimatedNodeType::Tracking;
+ assert(string == "color");
+ return AnimatedNodeType::Color;
};
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ColorAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/ColorAnimatedNode.cpp
new file mode 100644
index 00000000000..ec5f080ecc6
--- /dev/null
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/ColorAnimatedNode.cpp
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#include "pch.h"
+
+#include
+#include
+#include "ColorAnimatedNode.h"
+#include "NativeAnimatedNodeManager.h"
+
+namespace Microsoft::ReactNative {
+ColorAnimatedNode::ColorAnimatedNode(
+ int64_t tag,
+ const winrt::Microsoft::ReactNative::JSValueObject &config,
+ const std::shared_ptr &manager)
+ : AnimatedNode(tag, config, manager) {
+ m_rNodeId = config[s_rNodeName].AsInt32();
+ m_gNodeId = config[s_gNodeName].AsInt32();
+ m_bNodeId = config[s_bNodeName].AsInt32();
+ m_aNodeId = config[s_aNodeName].AsInt32();
+ m_nativeColor = config[s_nativeColorName].Copy();
+
+ if (!m_useComposition) {
+ TryApplyNativeColor();
+ } else {
+ assert(false && "ColorAnimatedNode not supported");
+ }
+}
+
+uint32_t ColorAnimatedNode::GetColor() {
+ uint32_t r = 0;
+ uint32_t g = 0;
+ uint32_t b = 0;
+ uint32_t a = 0;
+
+ if (const auto manager = m_manager.lock()) {
+ if (const auto rNode = manager->GetValueAnimatedNode(m_rNodeId)) {
+ r = std::clamp(static_cast(std::round(rNode->Value())), 0u, 255u);
+ }
+ if (const auto gNode = manager->GetValueAnimatedNode(m_gNodeId)) {
+ g = std::clamp(static_cast(std::round(gNode->Value())), 0u, 255u);
+ }
+ if (const auto bNode = manager->GetValueAnimatedNode(m_bNodeId)) {
+ b = std::clamp(static_cast(std::round(bNode->Value())), 0u, 255u);
+ }
+ if (const auto aNode = manager->GetValueAnimatedNode(m_aNodeId)) {
+ a = std::clamp(static_cast(std::round(aNode->Value() * 255)), 0u, 255u);
+ }
+ }
+
+ const auto result = (a << 24) | (r << 16) | (g << 8) | b;
+ return result;
+}
+
+void ColorAnimatedNode::TryApplyNativeColor() {
+ if (m_nativeColor.IsNull()) {
+ return;
+ }
+
+ const auto brush = BrushFromColorObject(m_nativeColor).try_as();
+ if (!brush) {
+ return;
+ }
+
+ if (const auto manager = m_manager.lock()) {
+ if (const auto rNode = manager->GetValueAnimatedNode(m_rNodeId)) {
+ rNode->RawValue(brush.Color().R);
+ }
+ if (const auto gNode = manager->GetValueAnimatedNode(m_gNodeId)) {
+ gNode->RawValue(brush.Color().G);
+ }
+ if (const auto bNode = manager->GetValueAnimatedNode(m_bNodeId)) {
+ bNode->RawValue(brush.Color().B);
+ }
+ if (const auto aNode = manager->GetValueAnimatedNode(m_aNodeId)) {
+ aNode->RawValue(static_cast(brush.Color().A) / 255);
+ }
+ }
+}
+
+} // namespace Microsoft::ReactNative
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ColorAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/ColorAnimatedNode.h
new file mode 100644
index 00000000000..917463f7872
--- /dev/null
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/ColorAnimatedNode.h
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+#include "AnimatedNode.h"
+
+namespace Microsoft::ReactNative {
+class ColorAnimatedNode final : public AnimatedNode {
+ public:
+ ColorAnimatedNode(
+ int64_t tag,
+ const winrt::Microsoft::ReactNative::JSValueObject &config,
+ const std::shared_ptr &manager);
+
+ uint32_t GetColor();
+
+ private:
+ void TryApplyNativeColor();
+
+ int32_t m_rNodeId{};
+ int32_t m_gNodeId{};
+ int32_t m_bNodeId{};
+ int32_t m_aNodeId{};
+ winrt::Microsoft::ReactNative::JSValue m_nativeColor{};
+
+ static constexpr std::string_view s_rNodeName{"r"};
+ static constexpr std::string_view s_gNodeName{"g"};
+ static constexpr std::string_view s_bNodeName{"b"};
+ static constexpr std::string_view s_aNodeName{"a"};
+ static constexpr std::string_view s_nativeColorName{"nativeColor"};
+};
+} // namespace Microsoft::ReactNative
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp
index 96beefa0137..f086f0aa1e3 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.cpp
@@ -7,8 +7,21 @@
#include "ExtrapolationType.h"
#include "InterpolationAnimatedNode.h"
#include "NativeAnimatedNodeManager.h"
+#include "Utils/ValueUtils.h"
namespace Microsoft::ReactNative {
+
+inline int32_t ColorToInt(winrt::Windows::UI::Color color) {
+ return static_cast(color.A) << 24 | static_cast(color.R) << 16 |
+ static_cast(color.G) << 8 | static_cast(color.B);
+}
+
+inline uint8_t ScaleByte(uint8_t min, uint8_t max, double ratio) {
+ const auto scaledValue = min + (max - min) * ratio;
+ const auto clampedValue = std::clamp(static_cast(std::round(scaledValue)), 0u, 255u);
+ return static_cast(clampedValue);
+}
+
InterpolationAnimatedNode::InterpolationAnimatedNode(
int64_t tag,
const winrt::Microsoft::ReactNative::JSValueObject &config,
@@ -17,8 +30,18 @@ InterpolationAnimatedNode::InterpolationAnimatedNode(
for (const auto &rangeValue : config[s_inputRangeName].AsArray()) {
m_inputRanges.push_back(rangeValue.AsDouble());
}
- for (const auto &rangeValue : config[s_outputRangeName].AsArray()) {
- m_outputRanges.push_back(rangeValue.AsDouble());
+
+ const auto isColorOutput = config[s_outputTypeName].AsString() == s_colorOutputType;
+ if (!m_useComposition && isColorOutput) {
+ m_isColorOutput = true;
+ for (const auto &rangeValue : config[s_outputRangeName].AsArray()) {
+ m_colorOutputRanges.push_back(ColorFrom(rangeValue));
+ }
+ } else {
+ assert(!isColorOutput && "Color interpolation not supported");
+ for (const auto &rangeValue : config[s_outputRangeName].AsArray()) {
+ m_defaultOutputRanges.push_back(rangeValue.AsDouble());
+ }
}
m_extrapolateLeft = config[s_extrapolateLeftName].AsString();
@@ -33,7 +56,11 @@ void InterpolationAnimatedNode::Update() {
if (const auto manager = m_manager.lock()) {
if (const auto node = manager->GetValueAnimatedNode(m_parentTag)) {
- RawValue(InterpolateValue(node->Value()));
+ if (m_isColorOutput) {
+ RawValue(InterpolateColor(node->Value()));
+ } else {
+ RawValue(InterpolateValue(node->Value()));
+ }
}
}
}
@@ -95,8 +122,9 @@ comp::ExpressionAnimation InterpolationAnimatedNode::CreateExpressionAnimation(
for (size_t i = 0; i < m_inputRanges.size(); i++) {
animation.SetScalarParameter(s_inputName.data() + std::to_wstring(i), static_cast(m_inputRanges[i]));
}
- for (size_t i = 0; i < m_outputRanges.size(); i++) {
- animation.SetScalarParameter(s_outputName.data() + std::to_wstring(i), static_cast(m_outputRanges[i]));
+ for (size_t i = 0; i < m_defaultOutputRanges.size(); i++) {
+ animation.SetScalarParameter(
+ s_outputName.data() + std::to_wstring(i), static_cast(m_defaultOutputRanges[i]));
}
return animation;
}
@@ -173,7 +201,7 @@ winrt::hstring InterpolationAnimatedNode::GetRightExpression(
const winrt::hstring &value,
const winrt::hstring &rightInterpolateExpression) {
const auto lastInput = s_inputName.data() + std::to_wstring(m_inputRanges.size() - 1);
- const auto lastOutput = s_outputName.data() + std::to_wstring(m_outputRanges.size() - 1);
+ const auto lastOutput = s_outputName.data() + std::to_wstring(m_defaultOutputRanges.size() - 1);
switch (ExtrapolationTypeFromString(m_extrapolateRight)) {
case ExtrapolationType::Clamp:
return value + L" > " + lastInput + L" ? " + lastOutput + L" : ";
@@ -200,10 +228,49 @@ double InterpolationAnimatedNode::InterpolateValue(double value) {
value,
m_inputRanges[index],
m_inputRanges[index + 1],
- m_outputRanges[index],
- m_outputRanges[index + 1],
+ m_defaultOutputRanges[index],
+ m_defaultOutputRanges[index + 1],
m_extrapolateLeft,
m_extrapolateRight);
}
+double InterpolationAnimatedNode::InterpolateColor(double value) {
+ // Compute range index
+ size_t index = 1;
+ for (; index < m_inputRanges.size() - 1; ++index) {
+ if (m_inputRanges[index] >= value) {
+ break;
+ }
+ }
+ index--;
+
+ double result;
+ const auto outputMin = m_colorOutputRanges[index];
+ const auto outputMax = m_colorOutputRanges[index + 1];
+ const auto outputMinInt = ColorToInt(outputMin);
+ const auto outputMaxInt = ColorToInt(outputMax);
+ if (outputMin == outputMax) {
+ memcpy(&result, &outputMinInt, sizeof(int32_t));
+ return result;
+ }
+
+ const auto inputMin = m_inputRanges[index];
+ const auto inputMax = m_inputRanges[index + 1];
+ if (inputMin == inputMax) {
+ if (value <= inputMin) {
+ memcpy(&result, &outputMinInt, sizeof(int32_t));
+ } else {
+ memcpy(&result, &outputMaxInt, sizeof(int32_t));
+ }
+ return result;
+ }
+
+ const auto ratio = (value - inputMin) / (inputMax - inputMin);
+ const auto interpolatedColor = ScaleByte(outputMin.A, outputMax.A, ratio) << 24 |
+ ScaleByte(outputMin.R, outputMax.R, ratio) << 16 | ScaleByte(outputMin.G, outputMax.G, ratio) << 8 |
+ ScaleByte(outputMin.B, outputMax.B, ratio);
+ memcpy(&result, &interpolatedColor, sizeof(int32_t));
+ return result;
+}
+
} // namespace Microsoft::ReactNative
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h
index 84321c4ee91..6d738584dd8 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/InterpolationAnimatedNode.h
@@ -17,6 +17,10 @@ class InterpolationAnimatedNode final : public ValueAnimatedNode {
virtual void OnDetachedFromNode(int64_t animatedNodeTag) override;
virtual void OnAttachToNode(int64_t animatedNodeTag) override;
+ bool IsColorValue() override {
+ return m_isColorOutput;
+ }
+
static constexpr std::string_view ExtrapolateTypeIdentity = "identity";
static constexpr std::string_view ExtrapolateTypeClamp = "clamp";
static constexpr std::string_view ExtrapolateTypeExtend = "extend";
@@ -35,11 +39,14 @@ class InterpolationAnimatedNode final : public ValueAnimatedNode {
winrt::hstring GetRightExpression(const winrt::hstring &, const winrt::hstring &rightInterpolateExpression);
double InterpolateValue(double value);
+ double InterpolateColor(double value);
comp::ExpressionAnimation m_rawValueAnimation{nullptr};
comp::ExpressionAnimation m_offsetAnimation{nullptr};
+ bool m_isColorOutput{false};
std::vector m_inputRanges;
- std::vector m_outputRanges;
+ std::vector m_defaultOutputRanges;
+ std::vector m_colorOutputRanges;
std::string m_extrapolateLeft;
std::string m_extrapolateRight;
@@ -49,9 +56,12 @@ class InterpolationAnimatedNode final : public ValueAnimatedNode {
static constexpr std::string_view s_inputRangeName{"inputRange"};
static constexpr std::string_view s_outputRangeName{"outputRange"};
+ static constexpr std::string_view s_outputTypeName{"outputType"};
static constexpr std::string_view s_extrapolateLeftName{"extrapolateLeft"};
static constexpr std::string_view s_extrapolateRightName{"extrapolateRight"};
+ static constexpr std::string_view s_colorOutputType{"color"};
+
static constexpr std::wstring_view s_parentPropsName{L"p"};
static constexpr std::wstring_view s_inputName{L"i"};
static constexpr std::wstring_view s_outputName{L"o"};
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp
index 2076e8672b6..fe0bc6fa44b 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp
@@ -118,6 +118,10 @@ void NativeAnimatedNodeManager::CreateAnimatedNode(
m_trackingNodes.emplace(tag, std::make_unique(tag, config, manager));
break;
}
+ case AnimatedNodeType::Color: {
+ m_colorNodes.emplace(tag, std::make_unique(tag, config, manager));
+ break;
+ }
default: {
assert(false);
break;
@@ -483,6 +487,9 @@ AnimatedNode *NativeAnimatedNodeManager::GetAnimatedNode(int64_t tag) {
if (m_trackingNodes.count(tag)) {
return m_trackingNodes.at(tag).get();
}
+ if (m_colorNodes.count(tag)) {
+ return m_colorNodes.at(tag).get();
+ }
return static_cast(nullptr);
}
@@ -521,6 +528,13 @@ TrackingAnimatedNode *NativeAnimatedNodeManager::GetTrackingAnimatedNode(int64_t
return nullptr;
}
+ColorAnimatedNode *NativeAnimatedNodeManager::GetColorAnimatedNode(int64_t tag) {
+ if (m_colorNodes.count(tag)) {
+ return m_colorNodes.at(tag).get();
+ }
+ return nullptr;
+}
+
void NativeAnimatedNodeManager::RemoveActiveAnimation(int64_t tag) {
m_activeAnimations.erase(tag);
}
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h
index 312ddb0bcd4..db21f7344c8 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h
@@ -10,6 +10,7 @@
#include
#include "AnimatedNode.h"
#include "AnimationDriver.h"
+#include "ColorAnimatedNode.h"
#include "EventAnimationDriver.h"
#include "PropsAnimatedNode.h"
#include "StyleAnimatedNode.h"
@@ -98,6 +99,7 @@ class NativeAnimatedNodeManager {
StyleAnimatedNode *GetStyleAnimatedNode(int64_t tag);
TransformAnimatedNode *GetTransformAnimatedNode(int64_t tag);
TrackingAnimatedNode *GetTrackingAnimatedNode(int64_t tag);
+ ColorAnimatedNode *GetColorAnimatedNode(int64_t tag);
void RemoveActiveAnimation(int64_t tag);
void RemoveStoppedAnimation(int64_t tag, const std::shared_ptr &manager);
@@ -122,6 +124,7 @@ class NativeAnimatedNodeManager {
std::unordered_map> m_styleNodes{};
std::unordered_map> m_transformNodes{};
std::unordered_map> m_trackingNodes{};
+ std::unordered_map> m_colorNodes{};
std::unordered_map, std::vector>>
m_eventDrivers{};
std::unordered_map> m_activeAnimations{};
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp
index 2511043c178..edcf9001169 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/PropsAnimatedNode.cpp
@@ -126,9 +126,28 @@ void PropsAnimatedNode::UpdateView() {
if (facade != FacadeType::None) {
MakeAnimation(entry.second, facade);
}
+ } else if (valueNode->IsColorValue()) {
+ const auto value = valueNode->Value();
+ int32_t color;
+ memcpy(&color, &value, sizeof(int32_t));
+ m_props[entry.first] = color;
} else {
m_props[entry.first] = valueNode->Value();
}
+ } else if (const auto &colorNode = manager->GetColorAnimatedNode(entry.second)) {
+ if (m_useComposition) {
+ const auto &facade = StringToFacadeType(entry.first);
+ if (facade != FacadeType::None) {
+ MakeAnimation(entry.second, facade);
+ }
+ } else if (valueNode->IsColorValue()) {
+ const auto value = valueNode->Value();
+ int32_t color;
+ memcpy(&color, &value, sizeof(int32_t));
+ m_props[entry.first] = color;
+ } else {
+ m_props[entry.first] = colorNode->GetColor();
+ }
}
}
}
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp
index ef5499166d6..a2fcd02e818 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/StyleAnimatedNode.cpp
@@ -27,7 +27,16 @@ void StyleAnimatedNode::CollectViewUpdates(winrt::Microsoft::ReactNative::JSValu
if (const auto transformNode = manager->GetTransformAnimatedNode(propMapping.second)) {
transformNode->CollectViewUpdates(propsMap);
} else if (const auto node = manager->GetValueAnimatedNode(propMapping.second)) {
- propsMap[propMapping.first] = node->Value();
+ if (node->IsColorValue()) {
+ const auto value = node->Value();
+ int32_t color;
+ memcpy(&color, &value, sizeof(int32_t));
+ propsMap[propMapping.first] = color;
+ } else {
+ propsMap[propMapping.first] = node->Value();
+ }
+ } else if (const auto node = manager->GetColorAnimatedNode(propMapping.second)) {
+ propsMap[propMapping.first] = node->GetColor();
}
}
}
diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h b/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h
index 96281f8edfe..ac0f38680dc 100644
--- a/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h
+++ b/vnext/Microsoft.ReactNative/Modules/Animated/ValueAnimatedNode.h
@@ -31,6 +31,10 @@ class ValueAnimatedNode : public AnimatedNode {
void OnValueUpdate();
void ValueListener(const ValueListenerCallback &callback);
+ virtual bool IsColorValue() {
+ return false;
+ }
+
comp::CompositionPropertySet PropertySet() {
return m_propertySet;
};