Skip to content

Commit 2d8b997

Browse files
committed
Add pattern component support
1 parent fd3d903 commit 2d8b997

7 files changed

Lines changed: 287 additions & 0 deletions

File tree

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
@@ -51,6 +51,10 @@ export const svgPropertiesRoutes = {
5151
Component: svgAnimatedProperties.Polyline,
5252
name: 'Polyline',
5353
},
54+
Pattern: {
55+
Component: svgAnimatedProperties.Pattern,
56+
name: 'Pattern',
57+
},
5458
LinearGradient: {
5559
Component: svgAnimatedProperties.LinearGradient,
5660
name: 'LinearGradient',
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,

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';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
import type { PatternProps } from 'react-native-svg';
4+
5+
// TODO: Fix me
6+
// @ts-ignore RNSVG doesn't export types for web, see https://github.com/software-mansion/react-native-svg/pull/2801
7+
import type { SvgStyleBuilderConfig } from './common';
8+
9+
// TODO: Fix me
10+
// @ts-ignore RNSVG doesn't export types for web, see https://github.com/software-mansion/react-native-svg/pull/2801
11+
export const SVG_PATTERN_PROPERTIES_CONFIG: SvgStyleBuilderConfig<PatternProps> =
12+
{
13+
x: true,
14+
y: true,
15+
width: true,
16+
height: true,
17+
patternUnits: {
18+
process: (patternUnits) => (patternUnits === 'userSpaceOnUse' ? 1 : 0),
19+
},
20+
patternContentUnits: {
21+
process: (patternContentUnits) =>
22+
patternContentUnits === 'userSpaceOnUse' ? 1 : 0,
23+
},
24+
};

0 commit comments

Comments
 (0)