Skip to content

Commit c7ef69e

Browse files
authored
Merge pull request #2052 from Shopify/animations
usePathValue
2 parents ae4914d + 1afd7fa commit c7ef69e

File tree

30 files changed

+461
-278
lines changed

30 files changed

+461
-278
lines changed

docs/docs/animations/hooks.md

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,77 @@ sidebar_label: Hooks
55
slug: /animations/hooks
66
---
77

8+
## usePathInterpolation
9+
10+
This hook interpolates between different path values based on a progress value, providing smooth transitions between the provided paths.
11+
12+
Paths need to be interpolatable, meaning they must contain the same number and types of commands. If the paths have different commands or different numbers of commands, the interpolation may not behave as expected. Ensure that all paths in the `outputRange` array are structured similarly for proper interpolation.
13+
14+
```tsx twoslash
15+
import React, { useEffect } from 'react';
16+
import { useSharedValue, withTiming } from 'react-native-reanimated';
17+
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';
18+
19+
const angryPath = Skia.Path.MakeFromSVGString("M 16 25 C 32 27 43 28 49 28 C 54 28 62 28 73 26 C 66 54 60 70 55 74 C 51 77 40 75 27 55 C 25 50 21 40 27 55 L 16 25 Z")!;
20+
const normalPath = Skia.Path.MakeFromSVGString("M 21 31 C 31 32 39 32 43 33 C 67 35 80 36 81 38 C 84 42 74 57 66 60 C 62 61 46 59 32 50 C 24 44 20 37 21 31 Z")!;
21+
const goodPath = Skia.Path.MakeFromSVGString("M 21 45 C 21 37 24 29 29 25 C 34 20 38 18 45 18 C 58 18 69 30 69 45 C 69 60 58 72 45 72 C 32 72 21 60 21 45 Z")!;
22+
23+
const Demo = () => {
24+
const progress = useSharedValue(0);
25+
useEffect(() => {
26+
progress.value = withTiming(1, { duration: 1000 });
27+
}, []);
28+
29+
const path = usePathInterpolation(progress, [0, 0.5, 1], [angryPath, normalPath, goodPath]);
30+
return (
31+
<Canvas style={{ flex: 1 }}>
32+
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
33+
</Canvas>
34+
);
35+
};
36+
```
37+
38+
## usePathValue
39+
40+
This hooks offers an easy way to animate paths.
41+
Behind the scene, it make sure that everything is done as efficiently as possible.
42+
43+
```tsx twoslash
44+
import {useSharedValue, withSpring} from "react-native-reanimated";
45+
import {Gesture, GestureDetector} from "react-native-gesture-handler";
46+
import {usePathValue, Canvas, Path, processTransform3d, Skia} from "@shopify/react-native-skia";
47+
48+
const rrct = Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10);
49+
50+
export const FrostedCard = () => {
51+
const rotateY = useSharedValue(0);
52+
53+
const gesture = Gesture.Pan().onChange((event) => {
54+
rotateY.value -= event.changeX / 300;
55+
});
56+
57+
const clip = usePathValue((path) => {
58+
"worklet";
59+
path.addRRect(rrct);
60+
path.transform(
61+
processTransform3d([
62+
{ translate: [50, 50] },
63+
{ perspective: 300 },
64+
{ rotateY: rotateY.value },
65+
{ translate: [-50, -50] },
66+
])
67+
);
68+
});
69+
return (
70+
<GestureDetector gesture={gesture}>
71+
<Canvas style={{ flex: 1 }}>
72+
<Path path={clip} />
73+
</Canvas>
74+
</GestureDetector>
75+
);
76+
};
77+
```
78+
879
## useClock
980

1081
This hook returns a number indicating the time in milliseconds since the hook was activated.
@@ -32,31 +103,20 @@ export default function App() {
32103
}
33104
```
34105

35-
## usePathInterpolation
36-
37-
This hook interpolates between different path values based on a progress value, providing smooth transitions between the provided paths.
106+
## Canvas Size
38107

39-
Paths need to be interpolatable, meaning they must contain the same number and types of commands. If the paths have different commands or different numbers of commands, the interpolation may not behave as expected. Ensure that all paths in the `outputRange` array are structured similarly for proper interpolation.
108+
The Canvas element has an `onSize` property that can receive a shared value, which will be updated whenever the canvas size changes.
40109

41110
```tsx twoslash
42-
import React, { useEffect } from 'react';
43-
import { useSharedValue, withTiming } from 'react-native-reanimated';
44-
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';
45-
46-
const angryPath = Skia.Path.MakeFromSVGString("M 16 25 C 32 27 43 28 49 28 C 54 28 62 28 73 26 C 66 54 60 70 55 74 C 51 77 40 75 27 55 C 25 50 21 40 27 55 L 16 25 Z")!;
47-
const normalPath = Skia.Path.MakeFromSVGString("M 21 31 C 31 32 39 32 43 33 C 67 35 80 36 81 38 C 84 42 74 57 66 60 C 62 61 46 59 32 50 C 24 44 20 37 21 31 Z")!;
48-
const goodPath = Skia.Path.MakeFromSVGString("M 21 45 C 21 37 24 29 29 25 C 34 20 38 18 45 18 C 58 18 69 30 69 45 C 69 60 58 72 45 72 C 32 72 21 60 21 45 Z")!;
111+
import {useSharedValue} from "react-native-reanimated";
112+
import {Fill, Canvas} from "@shopify/react-native-skia";
49113

50114
const Demo = () => {
51-
const progress = useSharedValue(0);
52-
useEffect(() => {
53-
progress.value = withTiming(1, { duration: 1000 });
54-
}, []);
55-
56-
const path = usePathInterpolation(progress, [0, 0.5, 1], [angryPath, normalPath, goodPath]);
115+
// size will be updated as the canvas size changes
116+
const size = useSharedValue({ width: 0, height: 0 });
57117
return (
58-
<Canvas style={{ flex: 1 }}>
59-
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
118+
<Canvas style={{ flex: 1 }} onSize={size}>
119+
<Fill color="white" />
60120
</Canvas>
61121
);
62122
};

docs/docs/animations/reanimated3.md

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -45,60 +45,4 @@ export const HelloWorld = () => {
4545
};
4646
```
4747

48-
## Performance
49-
50-
When animating with React Native Skia, we recommend avoiding new JSI allocations on each frame. Instead of creating a new value on each frame to notify Reanimated that the value has changed, directly mutate the value and notify Reanimated. Below are examples illustrating this pattern:
51-
52-
```tsx twoslash
53-
import {Gesture} from "react-native-gesture-handler";
54-
import {useSharedValue} from "react-native-reanimated";
55-
import {Skia, notifyChange} from "@shopify/react-native-skia";
56-
57-
const matrix = useSharedValue(Skia.Matrix());
58-
const path = useSharedValue(Skia.Path.Make().moveTo(0, 0));
59-
60-
const pan = Gesture.Pan().onChange((e) => {
61-
// ❌ Avoid creating a new path on every frame
62-
const newPath = path.value.copy();
63-
path.value = newPath.lineTo(e.changeX, e.changeY);
64-
});
65-
66-
const pan2 = Gesture.Pan().onChange((e) => {
67-
// ✅ Instead, mutate the value directly and notify Reanimated
68-
path.value.lineTo(e.changeX, e.changeY);
69-
notifyChange(path);
70-
});
71-
72-
const pinch = Gesture.Pinch().onChange((e) => {
73-
// ❌ Avoid creating a new matrix on every frame
74-
const newMatrix = Skia.Matrix(matrix.value.get());
75-
matrix.value = newMatrix.scale(e.scale);
76-
});
77-
78-
const pinch2 = Gesture.Pinch().onChange((e) => {
79-
// ✅ Mutate the value and notify Reanimated
80-
matrix.value.scale(e.scale);
81-
notifyChange(matrix);
82-
});
83-
```
84-
85-
`path.interpolate` now has an extra parameter to interpolate paths without allocating new paths. We provide a [usePathInterpolation](/docs/animations/hooks#usepathinterpolation) hook that will do all the heavy lifting for you.
86-
87-
## Canvas Size
88-
89-
The Canvas element has an `onSize` property that can receive a shared value, which will be updated whenever the canvas size changes.
90-
91-
```tsx twoslash
92-
import {useSharedValue} from "react-native-reanimated";
93-
import {Fill, Canvas} from "@shopify/react-native-skia";
94-
95-
const Demo = () => {
96-
// size will be updated as the canvas size changes
97-
const size = useSharedValue({ width: 0, height: 0 });
98-
return (
99-
<Canvas style={{ flex: 1 }} onSize={size}>
100-
<Fill color="white" />
101-
</Canvas>
102-
);
103-
};
104-
```
48+
We offer some [Skia specific animation hooks](/docs/animations/hooks), especially for paths.

example/src/Examples/API/PathEffect.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
sub,
88
Canvas,
99
Circle,
10-
translate,
1110
Skia,
1211
PaintStyle,
1312
DiscretePathEffect,
@@ -22,6 +21,8 @@ import {
2221
processTransform2d,
2322
} from "@shopify/react-native-skia";
2423

24+
import { translate } from "../../components/Animations";
25+
2526
import { Title } from "./components/Title";
2627

2728
const path = Skia.Path.MakeFromSVGString(

example/src/Examples/API/components/drawings/backface.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
Group,
66
Path,
77
Rect,
8-
translate,
98
} from "@shopify/react-native-skia";
109

10+
import { translate } from "../../../../components/Animations";
11+
1112
const aspectRatio = 757 / 492;
1213
const center = { x: 492 / 2, y: 757 / 2 };
1314
export const CARD_WIDTH = 300;

example/src/Examples/FrostedCard/FrostedCard.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@ import {
77
BackdropFilter,
88
Fill,
99
Blur,
10-
notifyChange,
10+
usePathValue,
1111
} from "@shopify/react-native-skia";
1212
import React from "react";
1313
import { Dimensions, View } from "react-native";
1414
import { Gesture, GestureDetector } from "react-native-gesture-handler";
15-
import {
16-
useDerivedValue,
17-
useSharedValue,
18-
withSpring,
19-
} from "react-native-reanimated";
15+
import { useSharedValue, withSpring } from "react-native-reanimated";
2016

2117
const { width, height } = Dimensions.get("window");
2218
const CARD_WIDTH = width * 0.9;
@@ -47,7 +43,6 @@ export const FrostedCard = () => {
4743
const image = useImage(require("./dynamo.jpg"));
4844
const rotateX = useSharedValue(0);
4945
const rotateY = useSharedValue(0);
50-
const path = useSharedValue(Skia.Path.Make());
5146

5247
const gesture = Gesture.Pan()
5348
.onChange((event) => {
@@ -59,10 +54,10 @@ export const FrostedCard = () => {
5954
rotateY.value = withSpring(0, springConfig(velocityX / sf));
6055
});
6156

62-
useDerivedValue(() => {
63-
path.value.reset();
64-
path.value.addRRect(rrct);
65-
path.value.transform(
57+
const clip = usePathValue((path) => {
58+
"worklet";
59+
path.addRRect(rrct);
60+
path.transform(
6661
processTransform3d([
6762
{ translate: [width / 2, height / 2] },
6863
{ perspective: 300 },
@@ -71,9 +66,7 @@ export const FrostedCard = () => {
7166
{ translate: [-width / 2, -height / 2] },
7267
])
7368
);
74-
notifyChange(path);
7569
});
76-
7770
return (
7871
<View style={{ flex: 1 }}>
7972
<GestureDetector gesture={gesture}>
@@ -86,7 +79,7 @@ export const FrostedCard = () => {
8679
height={height}
8780
fit="cover"
8881
/>
89-
<BackdropFilter filter={<Blur blur={30} mode="clamp" />} clip={path}>
82+
<BackdropFilter filter={<Blur blur={30} mode="clamp" />} clip={clip}>
9083
<Fill color="rgba(255, 255, 255, 0.1)" />
9184
</BackdropFilter>
9285
</Canvas>

example/src/Examples/Gooey/Gooey.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
Canvas,
66
Fill,
77
Skia,
8-
translate,
98
vec,
109
Group,
1110
PathOp,
@@ -25,6 +24,8 @@ import Animated, {
2524
} from "react-native-reanimated";
2625
import { Gesture, GestureDetector } from "react-native-gesture-handler";
2726

27+
import { translate } from "../../components/Animations";
28+
2829
import { Icon, R } from "./components/Icon";
2930
import { Hamburger } from "./components/Hamburger";
3031
import { BG, FG } from "./components/Theme";

example/src/Examples/Neumorphism/Dashboard/components/Button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import {
44
Group,
55
LinearGradient,
66
RadialGradient,
7-
translate,
87
vec,
98
} from "@shopify/react-native-skia";
109
import type { ReactNode } from "react";
1110
import React from "react";
1211

12+
import { translate } from "../../../../components/Animations";
13+
1314
export const BUTTON_SIZE = 62;
1415
const PADDING = 6;
1516

example/src/Examples/Neumorphism/Dashboard/components/Control.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
Shadow,
33
vec,
44
Group,
5-
translate,
65
Text,
76
Circle,
87
LinearGradient,
@@ -38,9 +37,9 @@ export const Control = ({
3837
if (font === null) {
3938
return null;
4039
}
41-
const labelWidth = font.getTextWidth(label);
40+
const labelWidth = font.measureText(label).width;
4241
return (
43-
<Group transform={translate({ x: x + 30, y: y + 30 })}>
42+
<Group transform={[{ translate: [x + 30, y + 30] }]}>
4443
<Text
4544
x={2 * r - labelWidth - 16}
4645
y={r + font.getSize() / 2}
@@ -68,7 +67,7 @@ export const Control = ({
6867
strokeWidth={1}
6968
/>
7069
</Group>
71-
<Group transform={translate({ x: r / 2, y: r / 2 })}>
70+
<Group transform={[{ translate: [r / 2, r / 2] }]}>
7271
<Group color="rgba(235, 235, 245, 0.6)">
7372
{active && (
7473
<LinearGradient

example/src/Examples/Neumorphism/Dashboard/components/ProgressBar.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
rrect,
55
Group,
66
LinearGradient,
7-
translate,
87
Circle,
98
Skia,
109
vec,
@@ -41,9 +40,9 @@ export const ProgressBar = ({ progress }: ProgressBarProps) => {
4140
if (font === null) {
4241
return null;
4342
}
44-
const textWidth = font.getTextWidth("00°C");
43+
const textWidth = font.measureText("00°C").width;
4544
return (
46-
<Group transform={translate({ x: 100, y: 223 })}>
45+
<Group transform={[{ translate: [100, 223] }]}>
4746
<Group>
4847
<LinearGradient
4948
start={vec(12, 12)}

example/src/Examples/Neumorphism/Dashboard/components/Slider.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
rrect,
66
RoundedRect,
77
Group,
8-
translate,
98
LinearGradient,
109
vec,
1110
} from "@shopify/react-native-skia";
@@ -28,7 +27,7 @@ export const Slider = ({ x, y, progress }: SliderProps) => {
2827
[progress]
2928
);
3029
return (
31-
<Group transform={translate({ x, y })}>
30+
<Group transform={[{ translate: [x, y] }]}>
3231
<Box box={rrect(rect(0, 3.5, 192, 8), 25, 25)} color="#1B1B1D">
3332
<BoxShadow
3433
dx={-1.25}

0 commit comments

Comments
 (0)