Skip to content

Commit 3b1cd57

Browse files
committed
feat: basic animation support
1 parent 8bcc9fe commit 3b1cd57

File tree

12 files changed

+314
-76
lines changed

12 files changed

+314
-76
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ These are the features that are "done", in that they pass basic testing. More co
3939
- [x] Important styles
4040
- [x] Important props
4141
- [ ] Safe area units
42-
- [ ] Em & `currentColor`
42+
- [x] Em & `currentColor`
4343
- [ ] CSS functions (min, max, platform functions, etc)
4444
- [ ] Metro
4545
- [ ] Update compiler to new syntax (switch tuples to objects)

cpp/Animations.cpp

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#include "Animations.hpp"
2+
#include "StyleResolver.hpp"
3+
#include <unordered_map>
4+
#include <mutex>
5+
6+
namespace reactnativecss::animations {
7+
8+
using AnyMap = ::margelo::nitro::AnyMap;
9+
using AnyValue = ::margelo::nitro::AnyValue;
10+
using AnyObject = ::margelo::nitro::AnyObject;
11+
12+
// Map to store the shared keyframes observables (one per keyframe name)
13+
static std::unordered_map<std::string, std::shared_ptr<reactnativecss::Observable<std::shared_ptr<AnyMap>>>> keyframesObservables;
14+
15+
// Map to store scope-specific computeds: Map<variableScope, Map<name, Computed<AnyMap>>>
16+
static std::unordered_map<std::string, std::unordered_map<std::string, std::shared_ptr<reactnativecss::Computed<std::shared_ptr<AnyMap>>>>> scopedComputeds;
17+
18+
static std::mutex keyframesMutex;
19+
20+
void setKeyframes(const std::string &name, const std::shared_ptr<AnyMap> &keyframes) {
21+
std::lock_guard<std::mutex> lock(keyframesMutex);
22+
23+
// Find or create the observable for this keyframe name
24+
auto it = keyframesObservables.find(name);
25+
26+
if (it != keyframesObservables.end()) {
27+
// Update existing observable
28+
it->second->set(keyframes);
29+
} else {
30+
// Create new observable
31+
keyframesObservables[name] = reactnativecss::Observable<std::shared_ptr<AnyMap>>::create(
32+
keyframes);
33+
}
34+
}
35+
36+
std::shared_ptr<AnyMap> getKeyframes(const std::string &name, const std::string &variableScope,
37+
reactnativecss::Effect::GetProxy &get) {
38+
std::lock_guard<std::mutex> lock(keyframesMutex);
39+
40+
// First, ensure the Observable exists for this keyframe name
41+
auto obsIt = keyframesObservables.find(name);
42+
if (obsIt == keyframesObservables.end()) {
43+
// Create a new observable with an empty AnyMap
44+
keyframesObservables[name] = reactnativecss::Observable<std::shared_ptr<AnyMap>>::create(
45+
AnyMap::make());
46+
obsIt = keyframesObservables.find(name);
47+
}
48+
49+
// Get the observable for this keyframe name
50+
auto observable = obsIt->second;
51+
52+
// Now check if a Computed exists within the variableScope for this name
53+
auto scopeIt = scopedComputeds.find(variableScope);
54+
if (scopeIt == scopedComputeds.end()) {
55+
// Create the scope map if it doesn't exist
56+
scopedComputeds[variableScope] = std::unordered_map<std::string, std::shared_ptr<reactnativecss::Computed<std::shared_ptr<AnyMap>>>>();
57+
scopeIt = scopedComputeds.find(variableScope);
58+
}
59+
60+
auto &scopeMap = scopeIt->second;
61+
auto computedIt = scopeMap.find(name);
62+
63+
if (computedIt == scopeMap.end()) {
64+
// Create a new Computed that gets the AnyMap from the observable and processes it
65+
auto computed = reactnativecss::Computed<std::shared_ptr<AnyMap>>::create(
66+
[observable, variableScope](const std::shared_ptr<AnyMap> &prev,
67+
reactnativecss::Effect::GetProxy &get) {
68+
// Get the raw keyframes from the observable
69+
auto rawKeyframes = get(*observable);
70+
71+
// Create a new AnyMap to hold the resolved keyframes
72+
auto resolvedKeyframes = AnyMap::make(rawKeyframes->getMap().size());
73+
74+
// Loop over the entries of the rawKeyframes
75+
for (const auto &entry: rawKeyframes->getMap()) {
76+
const std::string &key = entry.first;
77+
const AnyValue &value = entry.second;
78+
79+
// Each value should be an AnyObject, if not skip that entry
80+
if (!std::holds_alternative<AnyObject>(value)) {
81+
continue;
82+
}
83+
84+
const auto &frameMap = std::get<AnyObject>(value);
85+
86+
// Create a temporary map to hold resolved frame values
87+
std::unordered_map<std::string, AnyValue> resolvedFrameMap;
88+
resolvedFrameMap.reserve(frameMap.size());
89+
90+
// Loop over each entry of the frame and resolve values
91+
for (const auto &frameEntry: frameMap) {
92+
const std::string &frameKey = frameEntry.first;
93+
const AnyValue &frameValue = frameEntry.second;
94+
95+
// Resolve the value using StyleResolver
96+
AnyValue resolvedValue = margelo::nitro::cssnitro::StyleResolver::resolveStyle(
97+
frameValue, variableScope, get
98+
);
99+
100+
resolvedFrameMap[frameKey] = resolvedValue;
101+
}
102+
103+
// Apply style mapping to the resolved frame
104+
auto transformedFrame = margelo::nitro::cssnitro::StyleResolver::applyStyleMapping(
105+
resolvedFrameMap, variableScope, get
106+
);
107+
108+
// Convert the transformed frame back to an AnyObject
109+
AnyObject finalFrame;
110+
for (const auto &transformedEntry: transformedFrame->getMap()) {
111+
finalFrame[transformedEntry.first] = transformedEntry.second;
112+
}
113+
114+
// Set the resolved and transformed frame in the result
115+
resolvedKeyframes->setObject(key, finalFrame);
116+
}
117+
118+
return resolvedKeyframes;
119+
},
120+
AnyMap::make()
121+
);
122+
123+
scopeMap[name] = computed;
124+
computedIt = scopeMap.find(name);
125+
}
126+
127+
// Return get(computed) to subscribe to it
128+
return get(*computedIt->second);
129+
}
130+
131+
void deleteScope(const std::string &name) {
132+
std::lock_guard<std::mutex> lock(keyframesMutex);
133+
scopedComputeds.erase(name);
134+
}
135+
136+
} // namespace reactnativecss::animations

cpp/Animations.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include "Observable.hpp"
2+
#include "Computed.hpp"
3+
#include <NitroModules/AnyMap.hpp>
4+
#include <string>
5+
#include <memory>
6+
7+
namespace reactnativecss::animations {
8+
void
9+
setKeyframes(const std::string &name, const std::shared_ptr<margelo::nitro::AnyMap> &keyframes);
10+
11+
std::shared_ptr<margelo::nitro::AnyMap>
12+
getKeyframes(const std::string &name, const std::string &variableScope,
13+
reactnativecss::Effect::GetProxy &get);
14+
15+
void deleteScope(const std::string &variableScope);
16+
} // namespace reactnativecss::animations

cpp/Environment.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// filepath: /Users/mark/Developer/react-native-css-nitro/cpp/Environment.cpp
21
#include "Environment.hpp"
32

43
namespace reactnativecss {

cpp/HybridStyleRegistry.cpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "VariableContext.hpp"
99
#include "PseudoClasses.hpp"
1010
#include "JSLogger.hpp"
11+
#include "Animations.hpp"
1112

1213
#include <regex>
1314
#include <string>
@@ -16,12 +17,8 @@
1617
#include <optional>
1718
#include <unordered_map>
1819
#include <mutex>
19-
20-
// New: dynamic payloads (now handled by ShadowTreeUpdateManager)
2120
#include <folly/dynamic.h>
22-
23-
// React Tag
24-
#include <react/renderer/core/ReactPrimitives.h> // facebook::react::Tag
21+
#include <react/renderer/core/ReactPrimitives.h>
2522

2623

2724
namespace margelo::nitro::cssnitro {
@@ -345,4 +342,9 @@ namespace margelo::nitro::cssnitro {
345342
// TODO: Integrate with style computation/ShadowTreeUpdateManager if needed.
346343
}
347344

345+
void HybridStyleRegistry::setKeyframes(const std::string &name,
346+
const std::shared_ptr<AnyMap> &keyframes) {
347+
reactnativecss::animations::setKeyframes(name, keyframes);
348+
}
349+
348350
} // namespace margelo::nitro::cssnitro

cpp/HybridStyleRegistry.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,10 @@ namespace margelo::nitro::cssnitro {
3232

3333
~HybridStyleRegistry() override;
3434

35-
// Shorthand aliases
3635
using PropValue = std::variant<std::string, double, bool>;
3736
using PropPath = std::vector<std::string>;
3837
using SelectorAndArgs = std::tuple<std::string, std::vector<std::string>>;
3938

40-
// ----- public API forwarded to Impl -----
4139
void setClassname(const std::string &className,
4240
const std::vector<HybridStyleRule> &styleRule) override;
4341

@@ -73,6 +71,9 @@ namespace margelo::nitro::cssnitro {
7371
void
7472
setWindowDimensions(double width, double height, double scale, double fontScale) override;
7573

74+
void
75+
setKeyframes(const std::string &name, const std::shared_ptr<AnyMap> &keyframes) override;
76+
7677
protected:
7778
void loadHybridMethods() override;
7879

cpp/StyleResolver.cpp

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
#include "StyleResolver.hpp"
66
#include "StyleFunction.hpp"
7+
#include "Animations.hpp"
78
#include <variant>
9+
#include <unordered_set>
810

911
namespace margelo::nitro::cssnitro {
1012

13+
using AnyObject = ::margelo::nitro::AnyObject;
14+
1115
AnyValue StyleResolver::resolveStyle(
1216
const AnyValue &value,
1317
const std::string &variableScope,
@@ -31,5 +35,95 @@ namespace margelo::nitro::cssnitro {
3135
return value;
3236
}
3337

34-
} // namespace margelo::nitro::cssnitro
38+
std::shared_ptr<AnyMap> StyleResolver::applyStyleMapping(
39+
const std::unordered_map<std::string, AnyValue> &inputMap,
40+
const std::string &variableScope,
41+
typename reactnativecss::Effect::GetProxy &get
42+
) {
43+
static const std::unordered_set<std::string> transformProps = {
44+
"translateX", "translateY", "translateZ",
45+
"rotate", "rotateX", "rotateY", "rotateZ",
46+
"scaleX", "scaleY", "scaleZ",
47+
"skewX", "skewY",
48+
"perspective"
49+
};
50+
51+
auto anyMap = AnyMap::make(inputMap.size());
52+
53+
for (const auto &kv: inputMap) {
54+
// Handle animationName property
55+
if (kv.first == "animationName") {
56+
// animationName can be a string or a vector of strings
57+
if (std::holds_alternative<std::string>(kv.second)) {
58+
// Single animation name
59+
const std::string &animName = std::get<std::string>(kv.second);
60+
auto keyframes = reactnativecss::animations::getKeyframes(animName,
61+
variableScope, get);
62+
63+
// Set animationName to the resolved keyframes object
64+
anyMap->setObject("animationName", keyframes->getMap());
65+
} else if (std::holds_alternative<AnyArray>(kv.second)) {
66+
// Array of animation names
67+
const AnyArray &animNames = std::get<AnyArray>(kv.second);
68+
AnyArray keyframesArray;
69+
70+
for (const auto &animNameValue: animNames) {
71+
if (std::holds_alternative<std::string>(animNameValue)) {
72+
const std::string &animName = std::get<std::string>(animNameValue);
73+
auto keyframes = reactnativecss::animations::getKeyframes(animName,
74+
variableScope,
75+
get);
76+
keyframesArray.push_back(keyframes->getMap());
77+
}
78+
}
79+
80+
// Set animationName to the array of resolved keyframes
81+
anyMap->setArray("animationName", keyframesArray);
82+
} else {
83+
// Invalid type, don't set anything
84+
}
85+
continue;
86+
}
3587

88+
// Handle transform properties
89+
if (transformProps.count(kv.first) > 0) {
90+
AnyArray transformArray;
91+
92+
// Get existing transform array if it exists
93+
if (anyMap->contains("transform")) {
94+
transformArray = anyMap->getArray("transform");
95+
}
96+
97+
// Find the value in the array with the key matching kv.first and set it to kv.second
98+
bool foundTransform = false;
99+
for (size_t i = 0; i < transformArray.size(); i++) {
100+
if (std::holds_alternative<AnyObject>(transformArray[i])) {
101+
auto obj = std::get<AnyObject>(transformArray[i]);
102+
if (obj.count(kv.first) > 0) {
103+
obj[kv.first] = kv.second;
104+
transformArray[i] = obj;
105+
foundTransform = true;
106+
break;
107+
}
108+
}
109+
}
110+
111+
// If transform property not found in array, add a new transform object
112+
if (!foundTransform) {
113+
AnyObject transformObj;
114+
transformObj[kv.first] = kv.second;
115+
transformArray.emplace_back(transformObj);
116+
}
117+
118+
anyMap->setArray("transform", transformArray);
119+
continue;
120+
}
121+
122+
// For all other properties, just pass through as-is
123+
anyMap->setAny(kv.first, kv.second);
124+
}
125+
126+
return anyMap;
127+
}
128+
129+
} // namespace margelo::nitro::cssnitro

cpp/StyleResolver.hpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <string>
88
#include "Effect.hpp"
99
#include <NitroModules/AnyMap.hpp>
10+
#include <unordered_map>
1011

1112
namespace margelo::nitro {
1213
struct AnyValue;
@@ -17,6 +18,7 @@ namespace margelo::nitro::cssnitro {
1718

1819
using AnyValue = ::margelo::nitro::AnyValue;
1920
using AnyArray = ::margelo::nitro::AnyArray;
21+
using AnyMap = ::margelo::nitro::AnyMap;
2022

2123
class StyleResolver {
2224
public:
@@ -33,7 +35,22 @@ namespace margelo::nitro::cssnitro {
3335
const std::string &variableScope,
3436
typename reactnativecss::Effect::GetProxy &get
3537
);
38+
39+
/**
40+
* Apply style mapping to a map of styles, converting individual transform
41+
* properties (like scaleX, rotateZ, translateY) into a transform array.
42+
* Also handles animationName by fetching keyframes.
43+
*
44+
* @param inputMap The map of style properties to process
45+
* @param variableScope The variable scope context for resolving animations
46+
* @param get The Effect::GetProxy for reactive dependencies
47+
* @return A new AnyMap with transform properties mapped into a transform array
48+
*/
49+
static std::shared_ptr<AnyMap> applyStyleMapping(
50+
const std::unordered_map<std::string, AnyValue> &inputMap,
51+
const std::string &variableScope,
52+
typename reactnativecss::Effect::GetProxy &get
53+
);
3654
};
3755

3856
} // namespace margelo::nitro::cssnitro
39-

0 commit comments

Comments
 (0)