Skip to content

Commit

Permalink
Merge pull request #2052 from Shopify/animations
Browse files Browse the repository at this point in the history
usePathValue
  • Loading branch information
wcandillon authored Dec 13, 2023
2 parents ae4914d + 1afd7fa commit c7ef69e
Show file tree
Hide file tree
Showing 30 changed files with 461 additions and 278 deletions.
98 changes: 79 additions & 19 deletions docs/docs/animations/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,77 @@ sidebar_label: Hooks
slug: /animations/hooks
---

## usePathInterpolation

This hook interpolates between different path values based on a progress value, providing smooth transitions between the provided paths.

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.

```tsx twoslash
import React, { useEffect } from 'react';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';

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")!;
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")!;
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")!;

const Demo = () => {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 1000 });
}, []);

const path = usePathInterpolation(progress, [0, 0.5, 1], [angryPath, normalPath, goodPath]);
return (
<Canvas style={{ flex: 1 }}>
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
</Canvas>
);
};
```

## usePathValue

This hooks offers an easy way to animate paths.
Behind the scene, it make sure that everything is done as efficiently as possible.

```tsx twoslash
import {useSharedValue, withSpring} from "react-native-reanimated";
import {Gesture, GestureDetector} from "react-native-gesture-handler";
import {usePathValue, Canvas, Path, processTransform3d, Skia} from "@shopify/react-native-skia";

const rrct = Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10);

export const FrostedCard = () => {
const rotateY = useSharedValue(0);

const gesture = Gesture.Pan().onChange((event) => {
rotateY.value -= event.changeX / 300;
});

const clip = usePathValue((path) => {
"worklet";
path.addRRect(rrct);
path.transform(
processTransform3d([
{ translate: [50, 50] },
{ perspective: 300 },
{ rotateY: rotateY.value },
{ translate: [-50, -50] },
])
);
});
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Path path={clip} />
</Canvas>
</GestureDetector>
);
};
```

## useClock

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

## usePathInterpolation

This hook interpolates between different path values based on a progress value, providing smooth transitions between the provided paths.
## Canvas Size

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.
The Canvas element has an `onSize` property that can receive a shared value, which will be updated whenever the canvas size changes.

```tsx twoslash
import React, { useEffect } from 'react';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';

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")!;
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")!;
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")!;
import {useSharedValue} from "react-native-reanimated";
import {Fill, Canvas} from "@shopify/react-native-skia";

const Demo = () => {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 1000 });
}, []);

const path = usePathInterpolation(progress, [0, 0.5, 1], [angryPath, normalPath, goodPath]);
// size will be updated as the canvas size changes
const size = useSharedValue({ width: 0, height: 0 });
return (
<Canvas style={{ flex: 1 }}>
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
<Canvas style={{ flex: 1 }} onSize={size}>
<Fill color="white" />
</Canvas>
);
};
Expand Down
58 changes: 1 addition & 57 deletions docs/docs/animations/reanimated3.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,60 +45,4 @@ export const HelloWorld = () => {
};
```

## Performance

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:

```tsx twoslash
import {Gesture} from "react-native-gesture-handler";
import {useSharedValue} from "react-native-reanimated";
import {Skia, notifyChange} from "@shopify/react-native-skia";

const matrix = useSharedValue(Skia.Matrix());
const path = useSharedValue(Skia.Path.Make().moveTo(0, 0));

const pan = Gesture.Pan().onChange((e) => {
// ❌ Avoid creating a new path on every frame
const newPath = path.value.copy();
path.value = newPath.lineTo(e.changeX, e.changeY);
});

const pan2 = Gesture.Pan().onChange((e) => {
// ✅ Instead, mutate the value directly and notify Reanimated
path.value.lineTo(e.changeX, e.changeY);
notifyChange(path);
});

const pinch = Gesture.Pinch().onChange((e) => {
// ❌ Avoid creating a new matrix on every frame
const newMatrix = Skia.Matrix(matrix.value.get());
matrix.value = newMatrix.scale(e.scale);
});

const pinch2 = Gesture.Pinch().onChange((e) => {
// ✅ Mutate the value and notify Reanimated
matrix.value.scale(e.scale);
notifyChange(matrix);
});
```

`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.

## Canvas Size

The Canvas element has an `onSize` property that can receive a shared value, which will be updated whenever the canvas size changes.

```tsx twoslash
import {useSharedValue} from "react-native-reanimated";
import {Fill, Canvas} from "@shopify/react-native-skia";

const Demo = () => {
// size will be updated as the canvas size changes
const size = useSharedValue({ width: 0, height: 0 });
return (
<Canvas style={{ flex: 1 }} onSize={size}>
<Fill color="white" />
</Canvas>
);
};
```
We offer some [Skia specific animation hooks](/docs/animations/hooks), especially for paths.
3 changes: 2 additions & 1 deletion example/src/Examples/API/PathEffect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
sub,
Canvas,
Circle,
translate,
Skia,
PaintStyle,
DiscretePathEffect,
Expand All @@ -22,6 +21,8 @@ import {
processTransform2d,
} from "@shopify/react-native-skia";

import { translate } from "../../components/Animations";

import { Title } from "./components/Title";

const path = Skia.Path.MakeFromSVGString(
Expand Down
3 changes: 2 additions & 1 deletion example/src/Examples/API/components/drawings/backface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
Group,
Path,
Rect,
translate,
} from "@shopify/react-native-skia";

import { translate } from "../../../../components/Animations";

const aspectRatio = 757 / 492;
const center = { x: 492 / 2, y: 757 / 2 };
export const CARD_WIDTH = 300;
Expand Down
21 changes: 7 additions & 14 deletions example/src/Examples/FrostedCard/FrostedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@ import {
BackdropFilter,
Fill,
Blur,
notifyChange,
usePathValue,
} from "@shopify/react-native-skia";
import React from "react";
import { Dimensions, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import {
useDerivedValue,
useSharedValue,
withSpring,
} from "react-native-reanimated";
import { useSharedValue, withSpring } from "react-native-reanimated";

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

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

useDerivedValue(() => {
path.value.reset();
path.value.addRRect(rrct);
path.value.transform(
const clip = usePathValue((path) => {
"worklet";
path.addRRect(rrct);
path.transform(
processTransform3d([
{ translate: [width / 2, height / 2] },
{ perspective: 300 },
Expand All @@ -71,9 +66,7 @@ export const FrostedCard = () => {
{ translate: [-width / 2, -height / 2] },
])
);
notifyChange(path);
});

return (
<View style={{ flex: 1 }}>
<GestureDetector gesture={gesture}>
Expand All @@ -86,7 +79,7 @@ export const FrostedCard = () => {
height={height}
fit="cover"
/>
<BackdropFilter filter={<Blur blur={30} mode="clamp" />} clip={path}>
<BackdropFilter filter={<Blur blur={30} mode="clamp" />} clip={clip}>
<Fill color="rgba(255, 255, 255, 0.1)" />
</BackdropFilter>
</Canvas>
Expand Down
3 changes: 2 additions & 1 deletion example/src/Examples/Gooey/Gooey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Canvas,
Fill,
Skia,
translate,
vec,
Group,
PathOp,
Expand All @@ -25,6 +24,8 @@ import Animated, {
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";

import { translate } from "../../components/Animations";

import { Icon, R } from "./components/Icon";
import { Hamburger } from "./components/Hamburger";
import { BG, FG } from "./components/Theme";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import {
Group,
LinearGradient,
RadialGradient,
translate,
vec,
} from "@shopify/react-native-skia";
import type { ReactNode } from "react";
import React from "react";

import { translate } from "../../../../components/Animations";

export const BUTTON_SIZE = 62;
const PADDING = 6;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Shadow,
vec,
Group,
translate,
Text,
Circle,
LinearGradient,
Expand Down Expand Up @@ -38,9 +37,9 @@ export const Control = ({
if (font === null) {
return null;
}
const labelWidth = font.getTextWidth(label);
const labelWidth = font.measureText(label).width;
return (
<Group transform={translate({ x: x + 30, y: y + 30 })}>
<Group transform={[{ translate: [x + 30, y + 30] }]}>
<Text
x={2 * r - labelWidth - 16}
y={r + font.getSize() / 2}
Expand Down Expand Up @@ -68,7 +67,7 @@ export const Control = ({
strokeWidth={1}
/>
</Group>
<Group transform={translate({ x: r / 2, y: r / 2 })}>
<Group transform={[{ translate: [r / 2, r / 2] }]}>
<Group color="rgba(235, 235, 245, 0.6)">
{active && (
<LinearGradient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
rrect,
Group,
LinearGradient,
translate,
Circle,
Skia,
vec,
Expand Down Expand Up @@ -41,9 +40,9 @@ export const ProgressBar = ({ progress }: ProgressBarProps) => {
if (font === null) {
return null;
}
const textWidth = font.getTextWidth("00°C");
const textWidth = font.measureText("00°C").width;
return (
<Group transform={translate({ x: 100, y: 223 })}>
<Group transform={[{ translate: [100, 223] }]}>
<Group>
<LinearGradient
start={vec(12, 12)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
rrect,
RoundedRect,
Group,
translate,
LinearGradient,
vec,
} from "@shopify/react-native-skia";
Expand All @@ -28,7 +27,7 @@ export const Slider = ({ x, y, progress }: SliderProps) => {
[progress]
);
return (
<Group transform={translate({ x, y })}>
<Group transform={[{ translate: [x, y] }]}>
<Box box={rrect(rect(0, 3.5, 192, 8), 25, 25)} color="#1B1B1D">
<BoxShadow
dx={-1.25}
Expand Down
Loading

0 comments on commit c7ef69e

Please sign in to comment.