Skip to content

Commit

Permalink
Reanimated 2 integration (#1867)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Oct 24, 2023
1 parent 0121d4f commit 548f598
Show file tree
Hide file tree
Showing 66 changed files with 1,044 additions and 1,693 deletions.
176 changes: 144 additions & 32 deletions docs/docs/animations/animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,170 @@ sidebar_label: Animations
slug: /animations/animations
---

React Native Skia offers integration with [Reanimated v3](https://docs.swmansion.com/react-native-reanimated/), enabling animations to be executed on the UI-thread.

Note: This integration is available starting from Reanimated v3. If you are using Reanimated 2, refer to the [Reanimated 2 support section](#reanimated-2).

## Hello World

React Native Skia supports direct usage of Reanimated's shared and derived values as properties. There's no need for functions like `createAnimatedComponent` or `useAnimatedProps`; simply pass the reanimated values directly as properties.

```tsx twoslash
import {useEffect} from "react";
import {Canvas, Circle, Group} from "@shopify/react-native-skia";
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";

export const HelloWorld = () => {
const size = 256;
const r = useSharedValue(0);
const c = useDerivedValue(() => size - r.value);
useEffect(() => {
r.value = withRepeat(withTiming(size * 0.33, { duration: 1000 }), -1);
}, [r, size]);
return (
<Canvas style={{ flex: 1 }}>
<Group blendMode="multiply">
<Circle cx={r} cy={r} r={r} color="cyan" />
<Circle cx={c} cy={r} r={r} color="magenta" />
<Circle
cx={size/2}
cy={c}
r={r}
color="yellow"
/>
</Group>
</Canvas>
);
};
```

:::info

Currently, built-in Skia animations are dependant on the JS thread.
For UI-thread animations with Reanimated 3, see [Reanimated support](/docs/animations/reanimated).
It's important to note that Reanimated and Skia employ different color formats. For color interpolation in Skia, use `interpolateColors`. If you're using interpolateColor from Reanimated, ensure you convert it with `convertToRGBA` from Reanimated.

:::
```tsx twoslash
import {
interpolateColor,
useDerivedValue,
// In react-native-reanimated <= 3.1.0, convertToRGBA is not exported yet in the types
// @ts-ignore
convertToRGBA,
} from "react-native-reanimated";

To ease building animation, the library provides some utilities to help you. There are two types of utility functions - imperative functions and hooks.
const color = useDerivedValue(() =>
convertToRGBA(
interpolateColor(
0,
[0, 1],
["cyan", "magenta"]
)
)
);
```

If you have a Skia Value that you want to animate declaratively, a hook is the best choice.
:::

In the example below, we want the position of the rectangle to animate when we toggle a given value, and we want it to do this with a spring animation.
### Canvas Size

## Declarative animation
The Canvas element has a `onSize` property that can receive a shared value that will be updated when the canvas size changes.

```tsx twoslash
import React, { useState } from "react";
import { Canvas, Rect, useSpring } from "@shopify/react-native-skia";
import { Button, StyleSheet } from "react-native";
import {useSharedValue} from "react-native-reanimated";
import {Fill, Canvas} from "@shopify/react-native-skia";

export const AnimationExample = () => {
const [toggled, setToggled] = useState(false);
const position = useSpring(toggled ? 100 : 0);
const Demo = () => {
// size will be updated as the canvas size changes
const size = useSharedValue({ width: 0, height: 0 });
return (
<>
<Canvas style={{ flex: 1 }}>
<Rect x={position} y={100} width={10} height={10} color={"red"} />
</Canvas>
<Button title="Toggle" onPress={() => setToggled((p) => !p)} />
</>
<Canvas style={{ flex: 1 }} onSize={size}>
<Fill color="white" />
</Canvas>
);
};
```

## Imperative animation
## Reanimated 2

:::info

The Reanimated 2 integration operates on the JS thread. We recommend using Reanimated 3 when possible. For details, refer to the [Reanimated 3 support section](#hello-world).

:::

While we do provide a seamless integration with Reanimated v2, it comes with two caveats:
* [Animations are executed on the JS thread](#animations-on-the-js-thread)
* [Host Objects](#object-objects)

### Animations on the JS thread

Almost the same thing can be accomplished with an imperative function (except that we are not toggling back and forth in this example):
In the example below, even though the animation is simple, it runs on the JS thread due to its use of Reanimated v2.

```tsx twoslash
import { Canvas, Rect, runSpring, useValue } from "@shopify/react-native-skia";
import { Button } from "react-native";
import {useEffect} from "react";
import {Canvas, Rect, mix, useValue} from "@shopify/react-native-skia";
import {useSharedValue, withRepeat, withTiming, useDerivedValue} from "react-native-reanimated";

const MyComponent = () => {
const x = useValue(0);
const progress = useSharedValue(0);

useEffect(() => {
progress.value = withRepeat(withTiming(1, { duration: 3000 }), -1, true);
}, [progress]);

useDerivedValue(() => {
return mix(progress.value, 0, 100);
});

export const AnimationExample = () => {
const position = useValue(0);
const changePosition = () => runSpring(position, 100);
return (
<>
<Canvas style={{ flex: 1 }}>
<Rect x={position} y={100} width={10} height={10} color={"red"} />
</Canvas>
<Button title="Toggle" onPress={changePosition} />
</>
<Canvas style={{ flex: 1 }}>
<Rect
x={x}
y={100}
width={10}
height={10}
color="red"
/>
</Canvas>
);
};
```

### Host Objects

When your animation invokes Skia objects with Reanimated v2, the code must explicitly run on the JS thread. To assist with this, we offer a `useDerivedValueOnJS` hook.

```tsx twoslash
import {useDerivedValueOnJS, Skia} from "@shopify/react-native-skia";
import {useDerivedValue} from "react-native-reanimated";

const path = Skia.Path.MakeFromSVGString("M 344 172 Q 344 167 343.793 163")!;
const path2 = Skia.Path.MakeFromSVGString("M 347 169 Q 344 167 349 164")!;

// ❌ this will crash as it's running on the worklet thread
useDerivedValue(() => path.interpolate(path2, 0.5));

// ✅ this will work as expected
useDerivedValueOnJS(() => path.interpolate(path2, 0.5));
```

Similarly, if you intend to use host objects inside a gesture handler, ensure its execution on the JS thread:

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

const pos = useSharedValue(vec(0, 0));

// ❌ this will crash as it's running on the worklet thread
Gesture.Pan().onChange(e => pos.value = vec(e.x, 0));

// ✅ this will work as expected
Gesture.Pan().runOnJS(true).onChange(e => pos.value = vec(e.x, 0));
```
110 changes: 75 additions & 35 deletions docs/docs/animations/gestures.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,90 @@
---
id: touch-events
title: Touch Events
sidebar_label: Touch Events
slug: /animations/touch-events
id: gestures
title: Gestures
sidebar_label: Gestures
slug: /animations/gestures
---

When integrating with [reanimated](/docs/animations/animations), we recommend using [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/).

:::info
We've prepared a few [tutorials](docs/tutorials#gestures) that showcase the use of advanced gestures within the context of Skia drawings.

Currently, built-in Skia animations are dependant on the JS thread.
For UI-thread animations with Reanimated 3, see [Reanimated support](/docs/animations/reanimated).
```tsx twoslash
import { useWindowDimensions } from "react-native";
import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import { useSharedValue, withDecay } from "react-native-reanimated";

export const AnimationWithTouchHandler = () => {
const { width } = useWindowDimensions();
const leftBoundary = 0;
const rightBoundary = width;
const translateX = useSharedValue(width / 2);

:::
const gesture = Gesture.Pan()
.onChange((e) => {
translateX.value += e.changeX;
})
.onEnd((e) => {
translateX.value = withDecay({
velocity: e.velocityX,
clamp: [leftBoundary, rightBoundary],
});
});

### useTouchHandler
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Circle cx={translateX} cy={40} r={20} color="#3E3E" />
</Canvas>
</GestureDetector>
);
};
```

The `useTouchHandler` hook handles touches in the `Canvas`.
It is meant to be used with values to animate canvas elements.
## Element Tracking
A common use-case involves activating gestures only for a specific element on the Canvas. The Gesture Handler excels in this area as it can account for all the transformations applied to an element, such as translations, scaling, and rotations. To track each element, overlay an animated view on it, ensuring that the same transformations applied to the canvas element are mirrored on the animated view.

The useTouchHandler hook provides you with callbacks for single touch events.
To track multiple touches use the `useMultiTouchHandler` hook instead - it has
the same API as the single touch hook.
In the example below, each circle is tracked separately by two gesture handlers.

```tsx twoslash
import {
Canvas,
Circle,
useTouchHandler,
useValue,
} from "@shopify/react-native-skia";

const MyComponent = () => {
const cx = useValue(100);
const cy = useValue(100);

const touchHandler = useTouchHandler({
onActive: ({ x, y }) => {
cx.current = x;
cy.current = y;
},
});
import { View } from "react-native";
import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import Animated, { useSharedValue, useAnimatedStyle } from "react-native-reanimated";

const radius = 30;

export const ElementTracking = () => {
// The position of the ball
const x = useSharedValue(100);
const y = useSharedValue(100);
// This style will be applied to the "invisible" animated view
// that overlays the ball
const style = useAnimatedStyle(() => ({
position: "absolute",
top: -radius,
left: -radius,
width: radius * 2,
height: radius * 2,
transform: [{ translateX: x.value }, { translateY: y.value }],
}));
// The gesture handler for the ball
const gesture = Gesture.Pan().onChange((e) => {
x.value += e.x;
y.value += e.y;
});
return (
<Canvas style={{ flex: 1 }} onTouch={touchHandler}>
<Circle cx={cx} cy={cy} r={10} color="red" />
</Canvas>
<View style={{ flex: 1 }}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Circle cx={x} cy={y} r={radius} color="cyan" />
</Canvas>
<GestureDetector gesture={gesture}>
<Animated.View style={style} />
</GestureDetector>
</View>
);
};
```
```
Loading

0 comments on commit 548f598

Please sign in to comment.