Skip to content

Commit f63fe70

Browse files
authored
feat: Support for remaining SVG stroke properties in CSS (#7808)
## Summary This PR adds support and examples for the remaining RN SVG stroke properties: - `strokeDashoffset` - `strokeLinecap` - `strokeLinejoin` - `strokeMiterlimit` (this one seems to have no effect and likely doesn't work at all in `react-native-svg` - even when I tested without any animation on examples from [this](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-miterlimit) website) - `vectorEffect1` ## Example recording https://github.com/user-attachments/assets/3f788f37-9e29-45e8-9c85-2dbd38255f41
1 parent 22113f1 commit f63fe70

File tree

12 files changed

+245
-71
lines changed

12 files changed

+245
-71
lines changed

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

Lines changed: 171 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,54 @@
1-
import type { CSSAnimationKeyframes } from 'react-native-reanimated';
1+
import type {
2+
CSSAnimationKeyframes,
3+
CSSAnimationProperties,
4+
} from 'react-native-reanimated';
25
import Animated from 'react-native-reanimated';
3-
import { Circle, type CircleProps, Svg } from 'react-native-svg';
6+
import type { StrokeProps } from 'react-native-svg';
7+
import { Circle, Path, Svg } from 'react-native-svg';
48

59
import { ExamplesScreen } from '@/apps/css/components';
610
import { colors } from '@/theme';
711

812
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
13+
const AnimatedPath = Animated.createAnimatedComponent(Path);
914

1015
export default function StrokeExample() {
1116
return (
1217
<ExamplesScreen<
13-
{ keyframes: CSSAnimationKeyframes<CircleProps>; props?: CircleProps },
14-
CircleProps
18+
{
19+
keyframes: CSSAnimationKeyframes<StrokeProps>;
20+
animationProps?: Omit<CSSAnimationProperties, 'animationName'>;
21+
props?: StrokeProps;
22+
render?: (
23+
props: StrokeProps & {
24+
animatedProps: CSSAnimationProperties<StrokeProps>;
25+
}
26+
) => JSX.Element;
27+
},
28+
StrokeProps
1529
>
16-
buildAnimation={({ keyframes }) => ({
30+
buildAnimation={({ keyframes, animationProps }) => ({
1731
animationName: keyframes,
1832
animationDuration: '1s',
1933
animationIterationCount: 'infinite',
2034
animationTimingFunction: 'linear',
35+
...animationProps,
2136
})}
22-
renderExample={({ animation, props }) => (
23-
<Svg height={100} viewBox="0 0 100 100" width={100}>
24-
<AnimatedCircle
25-
animatedProps={animation}
26-
cx={50}
27-
cy={50}
28-
fill={colors.primary}
29-
r={20}
30-
stroke={colors.primaryDark}
31-
{...props}
32-
/>
33-
</Svg>
34-
)}
37+
renderExample={({ animation, props, render }) =>
38+
render?.({ ...props, animatedProps: animation }) ?? (
39+
<Svg height={100} viewBox="0 0 100 100" width={100}>
40+
<AnimatedCircle
41+
animatedProps={animation}
42+
cx={50}
43+
cy={50}
44+
fill={colors.primary}
45+
r={20}
46+
stroke={colors.primaryDark}
47+
{...props}
48+
/>
49+
</Svg>
50+
)
51+
}
3552
sections={[
3653
{
3754
title: 'Stroke',
@@ -156,6 +173,142 @@ export default function StrokeExample() {
156173
},
157174
],
158175
},
176+
{
177+
title: 'strokeDashoffset',
178+
examples: [
179+
{
180+
keyframes: {
181+
to: {
182+
strokeDashoffset: 100,
183+
},
184+
},
185+
props: {
186+
strokeWidth: 5,
187+
strokeDasharray: 10,
188+
},
189+
title: 'Absolute value',
190+
description:
191+
'`strokeDashArray` is set to `10` and `strokeWidth` is set to `5`',
192+
},
193+
],
194+
},
195+
{
196+
title: 'strokeLinecap',
197+
examples: [
198+
{
199+
keyframes: {
200+
from: {
201+
strokeLinecap: 'butt',
202+
},
203+
'50%': {
204+
strokeLinecap: 'round',
205+
},
206+
to: {
207+
strokeLinecap: 'square',
208+
},
209+
},
210+
props: {
211+
strokeWidth: 15,
212+
strokeDasharray: 20,
213+
},
214+
title: 'Changing `strokeLinecap`',
215+
description:
216+
'`strokeDashArray` is set to `10` and `strokeWidth` is set to `5`',
217+
},
218+
],
219+
},
220+
{
221+
title: 'strokeLinejoin',
222+
examples: [
223+
{
224+
keyframes: {
225+
from: {
226+
strokeLinejoin: 'miter',
227+
},
228+
'50%': {
229+
strokeLinejoin: 'round',
230+
},
231+
to: {
232+
strokeLinejoin: 'bevel',
233+
},
234+
},
235+
render: (props) => (
236+
<Svg height={100} viewBox="0 0 6 6" width={100}>
237+
<AnimatedPath
238+
d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5"
239+
fill="none"
240+
stroke={colors.primaryDark}
241+
{...props}
242+
/>
243+
</Svg>
244+
),
245+
title: 'Changing `strokeLinejoin`',
246+
},
247+
],
248+
},
249+
// TODO - this prop doesn't work in RN SVG so the example is commented
250+
// out for now
251+
// {
252+
// title: 'strokeMiterlimit',
253+
// examples: [
254+
// {
255+
// CardComponent: VerticalExampleCard,
256+
// keyframes: {
257+
// from: {
258+
// strokeMiterlimit: 1,
259+
// },
260+
// to: {
261+
// strokeMiterlimit: 8,
262+
// },
263+
// },
264+
// render: (props) => (
265+
// <Svg height={150} viewBox="0 0 38 30" width={200}>
266+
// <AnimatedPath
267+
// fill="none"
268+
// stroke={colors.primaryDark}
269+
// strokeLinejoin="miter"
270+
// strokeMiterlimit={10}
271+
// d="M1,19 l7 ,-3 l7 ,3
272+
// m2, 0 l3.5 ,-3 l3.5 ,3
273+
// m2, 0 l2 ,-3 l2 ,3
274+
// m2, 0 l0.75,-3 l0.75,3
275+
// m2, 0 l0.5 ,-3 l0.5 ,3"
276+
// {...props}
277+
// />
278+
// </Svg>
279+
// ),
280+
// title: 'Changing `strokeMiterlimit`',
281+
// },
282+
// ],
283+
// },
284+
{
285+
title: 'vectorEffect',
286+
examples: [
287+
{
288+
keyframes: {
289+
from: {
290+
vectorEffect: 'none',
291+
},
292+
to: {
293+
vectorEffect: 'nonScalingStroke',
294+
},
295+
},
296+
render: (props) => (
297+
<Svg height={150} viewBox="0 0 400 240" width={200}>
298+
<AnimatedPath
299+
d="M10,20 L40,100 L39,200 z"
300+
fill="none"
301+
stroke={colors.primaryDark}
302+
strokeWidth={5}
303+
transform="translate(100,0) scale(4,1)"
304+
{...props}
305+
/>
306+
</Svg>
307+
),
308+
title: 'Changing `strokeMiterlimit`',
309+
},
310+
],
311+
},
159312
]}
160313
/>
161314
);

apps/common-app/src/apps/css/navigation/Navigator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { colors, flex, iconSizes, radius, spacing } from '@/theme';
1111
import type { FontVariant } from '@/types';
1212

1313
import BottomTabBar from './BottomTabBar';
14-
import { INITIAL_ROUTE_NAME, TAB_ROUTES } from './constants';
1514
import {
1615
LocalNavigationProvider,
1716
useLocalNavigationRef,
1817
} from './LocalNavigationProvider';
18+
import { INITIAL_ROUTE_NAME, TAB_ROUTES } from './routes';
1919
import { SearchScreen } from './search';
2020
import type { Routes } from './types';
2121
import { isRouteWithRoutes } from './utils';
Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1 @@
1-
import { faExchange, faFire } from '@fortawesome/free-solid-svg-icons';
2-
3-
import { animationRoutes, transitionRoutes } from '../examples';
4-
import type { TabRoute } from './types';
5-
61
export const BOTTOM_BAR_HEIGHT = 60;
7-
8-
// We use stack navigator to mimic the tab navigator, thus top-level routes will be
9-
// displayed as tabs in the bottom tab bar
10-
export const TAB_ROUTES = {
11-
Animations: {
12-
icon: faFire,
13-
name: 'Animations',
14-
routes: animationRoutes,
15-
},
16-
Transitions: {
17-
icon: faExchange,
18-
name: 'Transitions',
19-
routes: transitionRoutes,
20-
},
21-
} satisfies Record<string, TabRoute>;
22-
23-
export const INITIAL_ROUTE_NAME = Object.values(TAB_ROUTES)[0]?.name;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './constants';
22
export { default as Navigator } from './Navigator';
3+
export * from './routes';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { faExchange, faFire } from '@fortawesome/free-solid-svg-icons';
2+
3+
import { animationRoutes, transitionRoutes } from '../examples';
4+
import type { TabRoute } from './types';
5+
6+
// We use stack navigator to mimic the tab navigator, thus top-level routes will be
7+
// displayed as tabs in the bottom tab bar
8+
export const TAB_ROUTES = {
9+
Animations: {
10+
icon: faFire,
11+
name: 'Animations',
12+
routes: animationRoutes,
13+
},
14+
Transitions: {
15+
icon: faExchange,
16+
name: 'Transitions',
17+
routes: transitionRoutes,
18+
},
19+
} satisfies Record<string, TabRoute>;
20+
21+
export const INITIAL_ROUTE_NAME = Object.values(TAB_ROUTES)[0]?.name;

apps/common-app/src/apps/css/navigation/search/SearchScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
2020
import { Stagger } from '@/apps/css/components';
2121
import { spacing } from '@/theme';
2222

23-
import { BOTTOM_BAR_HEIGHT, INITIAL_ROUTE_NAME } from '../constants';
23+
import { BOTTOM_BAR_HEIGHT } from '../constants';
24+
import { INITIAL_ROUTE_NAME } from '../routes';
2425
import { searchRoutes } from './fuse';
2526
import PullToSearchIndicator from './PullToSearchIndicator';
2627
import SearchBar from './SearchBar';

packages/react-native-reanimated/Common/cpp/reanimated/CSS/svg/config/interpolators/common.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ const InterpolatorFactoriesRecord SVG_STROKE_INTERPOLATORS = {
3131
{"strokeOpacity", value<CSSDouble>(1)},
3232
{"strokeDasharray",
3333
value<SVGStrokeDashArray, CSSKeyword>(SVGStrokeDashArray())},
34-
{"strokeDashoffset", value<CSSDouble>(0)},
35-
{"strokeLinecap", value<CSSKeyword>("butt")},
36-
{"strokeLinejoin", value<CSSKeyword>("miter")},
34+
{"strokeDashoffset", value<SVGLength>(0)},
35+
{"strokeLinecap", value<CSSInteger>(0)},
36+
{"strokeLinejoin", value<CSSInteger>(0)},
3737
{"strokeMiterlimit", value<CSSDouble>(4)},
38-
{"vectorEffect", value<CSSKeyword>("none")},
38+
{"vectorEffect", value<CSSInteger>(0)},
3939
};
4040

4141
const InterpolatorFactoriesRecord SVG_CLIP_INTERPOLATORS = {

packages/react-native-reanimated/src/css/platform/native/configs/svg/common.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import type {
1414
StrokeProps,
1515
TouchableProps,
1616
TransformProps,
17-
VectorEffectProps,
1817
} from 'react-native-svg';
1918

2019
import {
20+
convertStringToNumber,
21+
processColorSVG,
2122
processOpacity,
2223
processStrokeDashArray,
2324
type StyleBuilderConfig,
2425
} from '../../style';
25-
import { processColorSVG, processFillRule } from '../../style';
2626

2727
const colorAttributes = { process: processColorSVG };
2828

@@ -33,7 +33,12 @@ const colorProps: StyleBuilderConfig<ColorProps> = {
3333
const fillProps: StyleBuilderConfig<FillProps> = {
3434
fill: colorAttributes,
3535
fillOpacity: { process: processOpacity },
36-
fillRule: { process: processFillRule },
36+
fillRule: {
37+
process: convertStringToNumber({
38+
evenodd: 0,
39+
nonzero: 1,
40+
}),
41+
},
3742
};
3843

3944
const stokeProps: StyleBuilderConfig<StrokeProps> = {
@@ -42,10 +47,31 @@ const stokeProps: StyleBuilderConfig<StrokeProps> = {
4247
strokeOpacity: { process: processOpacity },
4348
strokeDasharray: { process: processStrokeDashArray },
4449
strokeDashoffset: true,
45-
strokeLinecap: true,
46-
strokeLinejoin: true,
50+
strokeLinecap: {
51+
process: convertStringToNumber({
52+
butt: 0,
53+
square: 2,
54+
round: 1,
55+
}),
56+
},
57+
strokeLinejoin: {
58+
process: convertStringToNumber({
59+
miter: 0,
60+
bevel: 2,
61+
round: 1,
62+
}),
63+
},
4764
strokeMiterlimit: true,
48-
vectorEffect: true,
65+
vectorEffect: {
66+
process: convertStringToNumber({
67+
none: 0,
68+
default: 0,
69+
nonScalingStroke: 1,
70+
'non-scaling-stroke': 1,
71+
inherit: 2,
72+
uri: 3,
73+
}),
74+
},
4975
};
5076

5177
const clipProps: StyleBuilderConfig<ClipProps> = {
@@ -72,10 +98,6 @@ const transformProps: StyleBuilderConfig<TransformProps> = {
7298
transform: true, // TODO - add preprocessor
7399
};
74100

75-
const vectorEffectProps: StyleBuilderConfig<VectorEffectProps> = {
76-
vectorEffect: true,
77-
};
78-
79101
const responderProps: StyleBuilderConfig<
80102
Omit<ResponderProps, keyof GestureResponderHandlers>
81103
> = {
@@ -115,7 +137,6 @@ export const commonSvgProps = {
115137
...stokeProps,
116138
...clipProps,
117139
...transformProps,
118-
...vectorEffectProps,
119140
...responderProps,
120141
...commonMarkerProps,
121142
...commonMaskProps,

0 commit comments

Comments
 (0)