Skip to content

Commit 77d9373

Browse files
committed
Add pattern component support
1 parent 9eb529d commit 77d9373

8 files changed

Lines changed: 303 additions & 16 deletions

File tree

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ export const svgPropertiesRoutes = {
66
Component: svgAnimatedProperties.Circle,
77
name: 'Circle',
88
},
9+
Common: {
10+
name: 'Common',
11+
routes: {
12+
FillAndColor: {
13+
Component: svgAnimatedProperties.common.FillAndColor,
14+
name: 'Fill and Color',
15+
},
16+
Stroke: {
17+
Component: svgAnimatedProperties.common.Stroke,
18+
name: 'Stroke',
19+
},
20+
},
21+
},
922
Ellipse: {
1023
Component: svgAnimatedProperties.Ellipse,
1124
name: 'Ellipse',
@@ -30,6 +43,10 @@ export const svgPropertiesRoutes = {
3043
Component: svgAnimatedProperties.Path,
3144
name: 'Path',
3245
},
46+
Pattern: {
47+
Component: svgAnimatedProperties.Pattern,
48+
name: 'Pattern',
49+
},
3350
Polygon: {
3451
Component: svgAnimatedProperties.Polygon,
3552
name: 'Polygon',
@@ -50,17 +67,4 @@ export const svgPropertiesRoutes = {
5067
Component: svgAnimatedProperties.Text,
5168
name: 'Text',
5269
},
53-
Common: {
54-
name: 'Common',
55-
routes: {
56-
FillAndColor: {
57-
Component: svgAnimatedProperties.common.FillAndColor,
58-
name: 'Fill and Color',
59-
},
60-
Stroke: {
61-
Component: svgAnimatedProperties.common.Stroke,
62-
name: 'Stroke',
63-
},
64-
},
65-
},
6670
} satisfies Routes;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import type { PropsWithChildren } from 'react';
2+
import Animated, { type CSSAnimationKeyframes } from 'react-native-reanimated';
3+
// TODO: Fix me
4+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+
// @ts-ignore RNSVG doesn't export types for web, see https://github.com/software-mansion/react-native-svg/pull/2801
6+
import type { PatternProps } from 'react-native-svg';
7+
import { Defs, Pattern, Rect, Svg } from 'react-native-svg';
8+
9+
import { ExamplesScreen } from '@/apps/css/components';
10+
import { colors } from '@/theme';
11+
12+
const TypedDefs = Defs as React.ComponentType<PropsWithChildren>;
13+
const AnimatedPattern = Animated.createAnimatedComponent(Pattern);
14+
15+
type PatternExtraProps = {
16+
patternUnits?: 'userSpaceOnUse' | 'objectBoundingBox';
17+
tileWidth?: number;
18+
tileHeight?: number;
19+
};
20+
21+
// With userSpaceOnUse, numeric values are in viewBox user units, but percentages resolve
22+
// relative to the SVG element's physical layout size in screen pixels. These two coordinate
23+
// spaces only align when the SVG element's physical size matches the viewBox dimensions.
24+
const MIXED_UNITS_NOTE =
25+
"**Note:** With `userSpaceOnUse`, mixing absolute and percentage values is not fully supported — the absolute value is in viewBox user units, but the percentage resolves relative to the SVG element's physical layout size in screen pixels. These coordinate spaces only align when the two happen to match.";
26+
27+
export default function PatternExample() {
28+
return (
29+
<ExamplesScreen<
30+
{
31+
keyframes: CSSAnimationKeyframes<PatternProps>;
32+
props?: PatternExtraProps;
33+
},
34+
PatternProps
35+
>
36+
buildAnimation={({ keyframes }) => ({
37+
animationDirection: 'alternate',
38+
animationDuration: '2s',
39+
animationIterationCount: 'infinite',
40+
animationName: keyframes,
41+
animationTimingFunction: 'ease-in-out',
42+
})}
43+
renderExample={({ animation, props }) => (
44+
<Svg height={100} viewBox="0 0 100 100" width={100}>
45+
<TypedDefs>
46+
<AnimatedPattern
47+
animatedProps={animation}
48+
height={props?.tileHeight ?? 20}
49+
id="stripes"
50+
patternUnits={props?.patternUnits ?? 'userSpaceOnUse'}
51+
width={props?.tileWidth ?? 20}>
52+
<Rect fill={colors.primary} height={10} width={20} />
53+
<Rect fill={colors.primaryLight} height={10} width={20} y={10} />
54+
</AnimatedPattern>
55+
</TypedDefs>
56+
<Rect fill="url(#stripes)" height={100} width={100} />
57+
</Svg>
58+
)}
59+
tabs={[
60+
{
61+
name: 'Position',
62+
sections: [
63+
{
64+
examples: [
65+
{
66+
description:
67+
'With `userSpaceOnUse`, x is in the viewBox coordinate space. Animates from 5 to 20 viewBox user units.',
68+
keyframes: { from: { x: 5 }, to: { x: 20 } },
69+
props: {
70+
patternUnits: 'userSpaceOnUse',
71+
tileHeight: 20,
72+
tileWidth: 20,
73+
},
74+
title: 'Numeric (userSpaceOnUse)',
75+
},
76+
{
77+
description:
78+
"With `objectBoundingBox`, x is a fraction of the bounding box of the element referencing the pattern. `0.05` = 5% and `0.2` = 20% of the referencing element's width.",
79+
keyframes: { from: { x: 0.05 }, to: { x: 0.2 } },
80+
props: {
81+
patternUnits: 'objectBoundingBox',
82+
tileHeight: 0.2,
83+
tileWidth: 0.2,
84+
},
85+
title: 'Numeric (objectBoundingBox)',
86+
},
87+
{
88+
description:
89+
"Percentage values resolve relative to the SVG element's physical layout width in screen pixels.",
90+
keyframes: { from: { x: '5%' }, to: { x: '20%' } },
91+
title: 'Percentage',
92+
},
93+
{
94+
description: MIXED_UNITS_NOTE,
95+
keyframes: { from: { x: 15 }, to: { x: '30%' } },
96+
title: 'Mixed',
97+
},
98+
],
99+
title: 'X Offset',
100+
},
101+
{
102+
examples: [
103+
{
104+
description:
105+
'With `userSpaceOnUse`, y is in the viewBox coordinate space. Animates from 5 to 20 viewBox user units.',
106+
keyframes: { from: { y: 5 }, to: { y: 20 } },
107+
props: {
108+
patternUnits: 'userSpaceOnUse',
109+
tileHeight: 20,
110+
tileWidth: 20,
111+
},
112+
title: 'Numeric (userSpaceOnUse)',
113+
},
114+
{
115+
description:
116+
"With `objectBoundingBox`, y is a fraction of the bounding box of the element referencing the pattern. `0.05` = 5% and `0.2` = 20% of the referencing element's height.",
117+
keyframes: { from: { y: 0.05 }, to: { y: 0.2 } },
118+
props: {
119+
patternUnits: 'objectBoundingBox',
120+
tileHeight: 0.2,
121+
tileWidth: 0.2,
122+
},
123+
title: 'Numeric (objectBoundingBox)',
124+
},
125+
{
126+
description:
127+
"Percentage values resolve relative to the SVG element's physical layout height in screen pixels.",
128+
keyframes: { from: { y: '5%' }, to: { y: '20%' } },
129+
title: 'Percentage',
130+
},
131+
{
132+
description: MIXED_UNITS_NOTE,
133+
keyframes: { from: { y: 15 }, to: { y: '30%' } },
134+
title: 'Mixed',
135+
},
136+
],
137+
title: 'Y Offset',
138+
},
139+
],
140+
},
141+
{
142+
name: 'Size',
143+
sections: [
144+
{
145+
examples: [
146+
{
147+
description:
148+
'With `userSpaceOnUse`, tile width is in the viewBox coordinate space. Animates from 15 to 30 viewBox user units.',
149+
keyframes: { from: { width: 15 }, to: { width: 30 } },
150+
props: { patternUnits: 'userSpaceOnUse' },
151+
title: 'Numeric (userSpaceOnUse)',
152+
},
153+
{
154+
description:
155+
"With `objectBoundingBox`, tile width is a fraction of the bounding box of the element referencing the pattern. `0.15` = 15% and `0.3` = 30% of the referencing element's width.",
156+
keyframes: { from: { width: 0.15 }, to: { width: 0.3 } },
157+
props: { patternUnits: 'objectBoundingBox' },
158+
title: 'Numeric (objectBoundingBox)',
159+
},
160+
{
161+
description:
162+
"Percentage values resolve relative to the SVG element's physical layout width in screen pixels.",
163+
keyframes: {
164+
from: { width: '15%' },
165+
to: { width: '30%' },
166+
},
167+
title: 'Percentage',
168+
},
169+
{
170+
description: MIXED_UNITS_NOTE,
171+
keyframes: { from: { width: 15 }, to: { width: '30%' } },
172+
title: 'Mixed',
173+
},
174+
],
175+
title: 'Tile Width',
176+
},
177+
{
178+
examples: [
179+
{
180+
description:
181+
'With `userSpaceOnUse`, tile height is in the viewBox coordinate space. Animates from 15 to 30 viewBox user units.',
182+
keyframes: { from: { height: 15 }, to: { height: 30 } },
183+
props: { patternUnits: 'userSpaceOnUse' },
184+
title: 'Numeric (userSpaceOnUse)',
185+
},
186+
{
187+
description:
188+
"With `objectBoundingBox`, tile height is a fraction of the bounding box of the element referencing the pattern. `0.15` = 15% and `0.3` = 30% of the referencing element's height.",
189+
keyframes: { from: { height: 0.15 }, to: { height: 0.3 } },
190+
props: { patternUnits: 'objectBoundingBox' },
191+
title: 'Numeric (objectBoundingBox)',
192+
},
193+
{
194+
description:
195+
"Percentage values resolve relative to the SVG element's physical layout height in screen pixels.",
196+
keyframes: {
197+
from: { height: '15%' },
198+
to: { height: '30%' },
199+
},
200+
title: 'Percentage',
201+
},
202+
{
203+
description: MIXED_UNITS_NOTE,
204+
keyframes: { from: { height: 15 }, to: { height: '30%' } },
205+
title: 'Mixed',
206+
},
207+
],
208+
title: 'Tile Height',
209+
},
210+
],
211+
},
212+
{
213+
name: 'Units',
214+
sections: [
215+
{
216+
examples: [
217+
{
218+
description:
219+
'With `patternUnits: "userSpaceOnUse"`, the pattern tile is defined in absolute user coordinates. Animating to `"objectBoundingBox"` reinterprets those same values as fractions of the bounding box, causing an abrupt jump.',
220+
keyframes: {
221+
from: {
222+
height: 20,
223+
patternUnits: 'userSpaceOnUse',
224+
width: 20,
225+
},
226+
to: {
227+
height: 20,
228+
patternUnits: 'objectBoundingBox',
229+
width: 20,
230+
},
231+
},
232+
title: 'patternUnits interpolation',
233+
},
234+
],
235+
title: 'Coordinate systems',
236+
},
237+
],
238+
},
239+
]}
240+
/>
241+
);
242+
}

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
@@ -6,6 +6,7 @@ import Image from './Image';
66
import Line from './Line';
77
import LinearGradient from './LinearGradient';
88
import Path from './Path';
9+
import Pattern from './Pattern';
910
import Polygon from './Polygon';
1011
import Polyline from './Polyline';
1112
import RadialGradient from './RadialGradient';
@@ -21,6 +22,7 @@ export default {
2122
Line,
2223
LinearGradient,
2324
Path,
25+
Pattern,
2426
Polygon,
2527
Polyline,
2628
RadialGradient,

apps/fabric-example/ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,17 @@ const InterpolatorFactoriesRecord SVG_PATH_INTERPOLATORS = mergeInterpolators(
352352
{"d", value<SVGPath>("")},
353353
}});
354354

355+
const InterpolatorFactoriesRecord SVG_PATTERN_INTERPOLATORS = mergeInterpolators(
356+
{SVG_COMMON_INTERPOLATORS,
357+
InterpolatorFactoriesRecord{
358+
{"x", value<CSSLength>(0, {RelativeTo::Parent, "width"})},
359+
{"y", value<CSSLength>(0, {RelativeTo::Parent, "height"})},
360+
{"width", value<CSSLength>(0, {RelativeTo::Parent, "width"})},
361+
{"height", value<CSSLength>(0, {RelativeTo::Parent, "height"})},
362+
{"patternUnits", value<CSSIndex>(0)},
363+
{"patternContentUnits", value<CSSIndex>(0)},
364+
}});
365+
355366
const InterpolatorFactoriesRecord SVG_RADIAL_GRADIENT_INTERPOLATORS = mergeInterpolators(
356367
{SVG_COMMON_INTERPOLATORS,
357368
InterpolatorFactoriesRecord{
@@ -398,6 +409,7 @@ ComponentInterpolatorsMap initializeRegistry() {
398409
registry["RNSVGLine"] = SVG_LINE_INTERPOLATORS;
399410
registry["RNSVGLinearGradient"] = SVG_LINEAR_GRADIENT_INTERPOLATORS;
400411
registry["RNSVGPath"] = SVG_PATH_INTERPOLATORS;
412+
registry["RNSVGPattern"] = SVG_PATTERN_INTERPOLATORS;
401413
registry["RNSVGRect"] = SVG_RECT_INTERPOLATORS;
402414
registry["RNSVGRadialGradient"] = SVG_RADIAL_GRADIENT_INTERPOLATORS;
403415
registry["RNSVGText"] = SVG_TEXT_INTERPOLATORS;

packages/react-native-reanimated/src/css/svg/init.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
SVG_LINE_PROPERTIES_CONFIG,
1212
SVG_LINEAR_GRADIENT_PROPERTIES_CONFIG,
1313
SVG_PATH_PROPERTIES_CONFIG,
14+
SVG_PATTERN_PROPERTIES_CONFIG,
1415
SVG_POLYGON_PROPERTIES_CONFIG,
1516
SVG_POLYLINE_PROPERTIES_CONFIG,
1617
SVG_RADIAL_GRADIENT_PROPERTIES_CONFIG,
@@ -32,6 +33,7 @@ export function initSvgCssSupport() {
3233
SVG_RADIAL_GRADIENT_PROPERTIES_CONFIG
3334
);
3435
registerComponentPropsBuilder('RNSVGPath', SVG_PATH_PROPERTIES_CONFIG);
36+
registerComponentPropsBuilder('RNSVGPattern', SVG_PATTERN_PROPERTIES_CONFIG);
3537
registerComponentPropsBuilder(
3638
getCompoundComponentName('RNSVGPath', 'Polygon'),
3739
SVG_POLYGON_PROPERTIES_CONFIG

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './image';
66
export * from './line';
77
export * from './linearGradient';
88
export * from './path';
9+
export * from './pattern';
910
export * from './polygon';
1011
export * from './polyline';
1112
export * from './radialGradient';

0 commit comments

Comments
 (0)