Skip to content

Commit 2652d71

Browse files
committed
Introduce SVGPath
1 parent 243a607 commit 2652d71

File tree

12 files changed

+1021
-3
lines changed

12 files changed

+1021
-3
lines changed

apps/common-app/src/apps/css/examples/animations/routes/properties/svg.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export const svgPropertiesRoutes = {
1818
name: 'Line',
1919
Component: svgAnimatedProperties.Line,
2020
},
21+
Path: {
22+
name: 'Path',
23+
Component: svgAnimatedProperties.Path,
24+
},
2125
Common: {
2226
name: 'Common',
2327
routes: {

apps/common-app/src/apps/css/examples/animations/screens/animatedProperties/svg/Path.tsx

Lines changed: 278 additions & 0 deletions
Large diffs are not rendered by default.

apps/common-app/src/apps/css/examples/animations/screens/animatedProperties/svg/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import common from './common';
33
import Ellipse from './Ellipse';
44
import Line from './Line';
55
import Rect from './Rect';
6+
import Path from './Path';
67

78
export default {
89
Circle,
910
Ellipse,
1011
Line,
12+
Path,
1113
common,
1214
Rect,
1315
};

packages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include <reanimated/CSS/svg/values/SVGBrush.h>
1717
#include <reanimated/CSS/svg/values/SVGLength.h>
18+
#include <reanimated/CSS/svg/values/SVGPath.h>
1819
#include <reanimated/CSS/svg/values/SVGStrokeDashArray.h>
1920

2021
#include <reanimated/CSS/interpolation/InterpolatorFactory.h>
@@ -341,7 +342,8 @@ const InterpolatorFactoriesRecord SVG_RECT_INTERPOLATORS = mergeInterpolators(
341342
const InterpolatorFactoriesRecord SVG_PATH_INTERPOLATORS = mergeInterpolators(
342343
{SVG_COMMON_INTERPOLATORS,
343344
InterpolatorFactoriesRecord{
344-
// TODO - add more properties
345+
{"d", value<SVGPath>("")},
346+
{"opacity", value<CSSDouble>(1)},
345347
}});
346348

347349
// ==================

packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <reanimated/CSS/common/values/complex/CSSBoxShadow.h>
1010
#include <reanimated/CSS/svg/values/SVGBrush.h>
1111
#include <reanimated/CSS/svg/values/SVGLength.h>
12+
#include <reanimated/CSS/svg/values/SVGPath.h>
1213
#include <reanimated/CSS/svg/values/SVGStrokeDashArray.h>
1314

1415
#include <worklets/Tools/JSISerializer.h>
@@ -168,6 +169,7 @@ template class CSSValueVariant<CSSDiscreteArray<CSSKeyword>>;
168169

169170
template class CSSValueVariant<SVGLength>;
170171
template class CSSValueVariant<SVGLength, CSSKeyword>;
172+
template class CSSValueVariant<SVGPath>;
171173
template class CSSValueVariant<SVGStrokeDashArray, CSSKeyword>;
172174
template class CSSValueVariant<SVGBrush>;
173175

packages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <reanimated/CSS/common/values/complex/CSSBoxShadow.h>
1414
#include <reanimated/CSS/svg/values/SVGBrush.h>
1515
#include <reanimated/CSS/svg/values/SVGLength.h>
16+
#include <reanimated/CSS/svg/values/SVGPath.h>
1617
#include <reanimated/CSS/svg/values/SVGStrokeDashArray.h>
1718

1819
#include <memory>
@@ -64,6 +65,7 @@ template class SimpleValueInterpolator<CSSDiscreteArray<CSSKeyword>>;
6465

6566
template class SimpleValueInterpolator<SVGLength>;
6667
template class SimpleValueInterpolator<SVGLength, CSSKeyword>;
68+
template class SimpleValueInterpolator<SVGPath>;
6769
template class SimpleValueInterpolator<SVGStrokeDashArray, CSSKeyword>;
6870
template class SimpleValueInterpolator<SVGBrush>;
6971

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
#include <SVGPath.h>
2+
#include <reanimated/CSS/svg/values/SVGPath.h>
3+
4+
#include <reanimated/CSS/common/transforms/vectors.h>
5+
6+
#include <cstddef>
7+
#include <functional>
8+
#include <optional>
9+
#include <string>
10+
11+
namespace reanimated::css {
12+
13+
using Point = Vector2D;
14+
using Cubic = std::array<Vector2D, 4>;
15+
using SubPath = SVGPath::SubPath;
16+
17+
SVGPath::SVGPath() : subPaths() {}
18+
19+
SVGPath::SVGPath(std::vector<SubPath> &&subPaths) : subPaths(std::move(subPaths)) {}
20+
21+
SVGPath::SVGPath(jsi::Runtime &rt, const jsi::Value &jsiValue) : SVGPath(jsiValue.getString(rt).utf8(rt)) {}
22+
23+
SVGPath::SVGPath(const folly::dynamic &value) : SVGPath(value.getString()) {}
24+
25+
bool SVGPath::canConstruct(jsi::Runtime &rt, const jsi::Value &jsiValue) {
26+
return jsiValue.isString();
27+
}
28+
29+
bool SVGPath::canConstruct(const folly::dynamic &value) {
30+
return value.isString();
31+
}
32+
33+
folly::dynamic SVGPath::toDynamic() const {
34+
return toString();
35+
}
36+
37+
std::string SVGPath::toString() const {
38+
std::ostringstream oss;
39+
oss << std::defaultfloat;
40+
for (size_t i = 0; i < subPaths.size(); ++i) {
41+
const auto &segment = subPaths[i];
42+
43+
if (i > 0) {
44+
oss << " ";
45+
}
46+
47+
oss << "M" << segment.M[0] << " " << segment.M[1];
48+
49+
for (const auto &point : segment.C) {
50+
oss << " C";
51+
for (int j = 1; j < 4; ++j) {
52+
// In SVGPath C command start point is implicit
53+
oss << " " << point[j][0] << " " << point[j][1];
54+
}
55+
}
56+
57+
if (segment.Z) {
58+
oss << " Z";
59+
}
60+
}
61+
return oss.str();
62+
}
63+
64+
SVGPath SVGPath::interpolate(const double progress, const SVGPath &to) const {
65+
const auto &longerPath = (subPaths.size() >= to.subPaths.size()) ? subPaths : to.subPaths;
66+
const auto &shorterPath = (subPaths.size() < to.subPaths.size()) ? subPaths : to.subPaths;
67+
68+
size_t longerSize = longerPath.size();
69+
size_t shorterSize = shorterPath.size();
70+
71+
std::vector<std::reference_wrapper<const SubPath>> fromRef;
72+
std::vector<std::reference_wrapper<const SubPath>> toRef;
73+
74+
fromRef.reserve(longerSize);
75+
toRef.reserve(longerSize);
76+
77+
if (shorterSize == 0) {
78+
return to;
79+
}
80+
81+
size_t baseGroupSize = longerSize / shorterSize;
82+
size_t remainder = longerSize % shorterSize;
83+
84+
size_t longerPathIndex = 0;
85+
86+
for (size_t i = 0; i < shorterSize; ++i) {
87+
size_t currentGroupSize = baseGroupSize + (i < remainder ? 1 : 0);
88+
89+
for (size_t j = 0; j < currentGroupSize; ++j) {
90+
if (subPaths.size() <= to.subPaths.size()) {
91+
fromRef.push_back(std::cref(subPaths[i]));
92+
toRef.push_back(std::cref(to.subPaths[longerPathIndex]));
93+
} else {
94+
fromRef.push_back(std::cref(subPaths[longerPathIndex]));
95+
toRef.push_back(std::cref(to.subPaths[i]));
96+
}
97+
longerPathIndex++;
98+
}
99+
}
100+
101+
std::vector<SubPath> interpolatedSubPaths;
102+
interpolatedSubPaths.reserve(longerSize);
103+
104+
for (size_t i = 0; i < longerSize; ++i) {
105+
interpolatedSubPaths.push_back(interpolateSubPaths(fromRef[i], toRef[i], progress));
106+
}
107+
108+
return SVGPath(std::move(interpolatedSubPaths));
109+
}
110+
111+
bool SVGPath::operator==(const SVGPath &other) const {
112+
return toString() == other.toString();
113+
}
114+
115+
#ifndef NDEBUG
116+
117+
std::ostream &operator<<(std::ostream &os, const SVGPath &value) {
118+
os << "SVGPath(" << value.toString() << ")";
119+
return os;
120+
}
121+
122+
#endif // NDEBUG
123+
124+
std::vector<SubPath> SVGPath::parseSVGPath(const std::string &value) const {
125+
std::vector<SubPath> result;
126+
std::stringstream ss(value);
127+
Point currPos;
128+
129+
// Format of input: (M num num |C num num num num num num |Z)*
130+
while (ss >> std::ws && !ss.eof()) {
131+
char cmd;
132+
ss >> cmd;
133+
134+
switch (cmd) {
135+
case 'M':
136+
double x, y;
137+
ss >> x >> y;
138+
result.emplace_back(Point(x, y));
139+
currPos = Point(x, y);
140+
break;
141+
case 'C':
142+
if (!result.empty()) {
143+
Point p0(currPos), p1, p2, p3;
144+
ss >> p1[0] >> p1[1] >> p2[0] >> p2[1] >> p3[0] >> p3[1];
145+
result.back().C.push_back({p0, p1, p2, p3});
146+
currPos = p3;
147+
break;
148+
}
149+
// Fallthrough
150+
case 'Z':
151+
if (!result.empty()) {
152+
result.back().Z = true;
153+
currPos = result.back().M;
154+
break;
155+
}
156+
// Fallthrough
157+
default:
158+
std::invalid_argument("[Reanimated] Invalid SVGPath string format.");
159+
}
160+
}
161+
162+
return result;
163+
}
164+
165+
std::vector<Cubic> SVGPath::splitCubic(Cubic cubic, int count) const {
166+
std::vector<Cubic> result;
167+
for (int i = 0; i < count; i++) {
168+
double t = 1.0 / (count - i);
169+
auto [st, nd] = singleSplitCubic(cubic, t);
170+
result.push_back(st);
171+
cubic = nd;
172+
}
173+
return result;
174+
}
175+
176+
SubPath SVGPath::interpolateSubPaths(const SubPath &from, const SubPath &to, double t) const {
177+
Point newM = from.M.interpolate(t, to.M);
178+
SubPath result(newM);
179+
180+
result.Z = to.Z;
181+
182+
size_t longerSize = std::max(from.C.size(), to.C.size());
183+
size_t shorterSize = std::min(from.C.size(), to.C.size());
184+
185+
if (shorterSize == 0) {
186+
return result;
187+
}
188+
189+
size_t baseGroupSize = longerSize / shorterSize;
190+
size_t remainder = longerSize % shorterSize;
191+
192+
std::vector<Cubic> prolongatedShorter;
193+
prolongatedShorter.reserve(longerSize);
194+
195+
for (size_t i = 0; i < shorterSize; ++i) {
196+
size_t currentGroupSize = baseGroupSize + (i < remainder ? 1 : 0);
197+
std::vector<Cubic> x =
198+
from.C.size() <= to.C.size() ? splitCubic(from.C[i], currentGroupSize) : splitCubic(to.C[i], currentGroupSize);
199+
prolongatedShorter.insert(prolongatedShorter.end(), x.begin(), x.end());
200+
}
201+
202+
auto &fromRef = from.C.size() <= to.C.size() ? prolongatedShorter : from.C;
203+
auto &toRef = from.C.size() <= to.C.size() ? to.C : prolongatedShorter;
204+
205+
for (size_t i = 0; i < longerSize; ++i) {
206+
const auto &c1 = fromRef[i];
207+
const auto &c2 = toRef[i];
208+
209+
Cubic newCubic;
210+
211+
for (size_t j = 0; j < 4; ++j) {
212+
newCubic[j] = c1[j].interpolate(t, c2[j]);
213+
}
214+
215+
// ensure continuity
216+
if (i == 0) {
217+
newCubic[0] = result.M;
218+
} else {
219+
newCubic[0] = result.C.back()[3];
220+
}
221+
222+
// Ensure tangent differs from 0
223+
{
224+
constexpr double NUDGE_EPS = 5e-1;
225+
newCubic[1] = applyDirectionalNudge(newCubic[1], newCubic[0], newCubic[2], newCubic[3], NUDGE_EPS);
226+
newCubic[2] = applyDirectionalNudge(newCubic[2], newCubic[3], newCubic[1], newCubic[0], NUDGE_EPS);
227+
}
228+
229+
result.C.push_back(newCubic);
230+
}
231+
232+
return result;
233+
}
234+
235+
Point SVGPath::lineAt(Point p0, Point p1, double t) const {
236+
return Point(p0[0] + t * (p1[0] - p0[0]), p0[1] + t * (p1[1] - p0[1]));
237+
}
238+
239+
Point SVGPath::applyDirectionalNudge(
240+
Point target,
241+
const Point &anchor,
242+
const Point &guide,
243+
const Point &altGuide,
244+
double epsilon) const {
245+
double dx0 = target[0] - anchor[0];
246+
double dy0 = target[1] - anchor[1];
247+
248+
if (dx0 * dx0 + dy0 * dy0 < epsilon * epsilon) {
249+
250+
Point v = Point(guide[0] - anchor[0], guide[1] - anchor[1]);
251+
double vLen = v.length();
252+
253+
if (vLen < epsilon) {
254+
v = Point(altGuide[0] - anchor[0], altGuide[1] - anchor[1]);
255+
vLen = v.length();
256+
}
257+
258+
if (vLen < epsilon) {
259+
target[0] += epsilon;
260+
return target;
261+
}
262+
263+
v.normalize();
264+
return Point(target[0] + v[0] * epsilon, target[1] + v[1] * epsilon);
265+
}
266+
267+
return target;
268+
}
269+
270+
std::pair<Cubic, Cubic> SVGPath::singleSplitCubic(const Cubic &p, double t) const {
271+
Point p01 = lineAt(p[0], p[1], t);
272+
Point p12 = lineAt(p[1], p[2], t);
273+
Point p23 = lineAt(p[2], p[3], t);
274+
Point c0 = lineAt(p01, p12, t);
275+
Point c1 = lineAt(p12, p23, t);
276+
Point q = lineAt(c0, c1, t);
277+
278+
return {{p[0], p01, c0, q}, {q, c1, p23, p[3]}};
279+
}
280+
281+
} // namespace reanimated::css

0 commit comments

Comments
 (0)