Skip to content

Commit

Permalink
feat: support reanimated and gesture handler (#11)
Browse files Browse the repository at this point in the history
* feat: setup support reanimated

* feat: example demo

* feat: support custom panGesture

* docs: update readme

* docs: improve installation step

* fix: remove npm stel

* chore: increase min required node version

* chore: remove gif example
  • Loading branch information
enzomanuelmangano authored Mar 6, 2024
1 parent 86447e9 commit 7864259
Show file tree
Hide file tree
Showing 17 changed files with 1,615 additions and 1,142 deletions.
Binary file removed .assets/example.gif
Binary file not shown.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.18.1
18.0.0
76 changes: 56 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ React Native Skia Gesture 🤌🏽

_A detection system for React Native Skia components._

<div align="center">
<img src="https://github.com/enzomanuelmangano/react-native-skia-gesture/blob/main/.assets/example.gif" title="react-native-skia-gesture">
</div>

### Motivation

React Native Skia, provides many declarative APIs for building screens using React Components. However, [Skia components are not real components, but abstract representations of a part of a drawing](https://github.com/Shopify/react-native-skia/issues/513#issuecomment-1290126304).
Expand All @@ -17,43 +13,49 @@ This package, simply provides a set of APIs to be able to interact directly with

## Installation

**You need to have already installed [@shopify/react-native-skia (>= 0.1.157)](https://shopify.github.io/react-native-skia/docs/getting-started/installation)**
**You need to have already installed [@shopify/react-native-skia (>= 0.1.221)](https://shopify.github.io/react-native-skia/docs/getting-started/installation)**

### Since v0.3.0

Open a Terminal in your project's folder and install the library using `yarn`:
You need to install Reanimated (v3.0.0+) and Gesture Handler (v2.0.0+)

```sh
yarn add react-native-skia-gesture
yarn add react-native-reanimated react-native-gesture-handler
```

or with `npm`:
### Install Skia Gesture

Open a Terminal in your project's folder and then install the library using `yarn`:

```sh
npm install react-native-skia-gesture
yarn add react-native-skia-gesture
```

## Usage

```jsx
import {
useValue,
} from '@shopify/react-native-skia';
useSharedValue,
} from 'react-native-reanimated';

import Touchable, {
useGestureHandler,
} from 'react-native-skia-gesture';

export default function App() {
const cx = useValue(100);
const cy = useValue(100);
const cx = useSharedValue(100);
const cy = useSharedValue(100);

const circleGesture = useGestureHandler<{ x: number; y: number }>({
onStart: (_, context) => {
context.x = cx.current;
context.y = cy.current;
'worklet'; // Remember the 'worklet' keyword
context.x = cx.value;
context.y = cy.value;
},
onActive: ({ translationX, translationY }, context) => {
cx.current = context.x + translationX;
cy.current = context.y + translationY;
'worklet';
cx.value = context.x + translationX;
cy.value = context.y + translationY;
},
});

Expand All @@ -69,9 +71,9 @@ If the element is a **Circle**, **Rect**, **RoundedRect**, or a **Path** the pac

```jsx
...
const touchablePath = useComputedValue(() => {
const touchablePath = useDerivedValue(() => {
const path = new Path();
path.addCircle(cx.current, cy.current, 50);
path.addCircle(cx.value, cy.value, 50);
return path;
}, [cx, cy]);

Expand All @@ -83,6 +85,40 @@ return (
...
```

### Run the Gestures on JS thread

You might want to run the gestures on the JS thread (but it's not recommended).
However it could be useful if you want to slowly migrate from the v0.2.0 to the v0.3.0.

To handle that you need to define the panGesture and pass it to the Touchable.Canvas

```jsx
import { Gesture } from 'react-native-gesture-handler';

...

const panGesture = Gesture.Pan().runOnJS(true)

const circleGesture = useGestureHandler<{ x: number; y: number }>({
// You can avoid the 'worklet' keyword if you are running the gesture on JS thread
onStart: (_, context) => {
context.x = cx.value;
context.y = cy.value;
},
onActive: ({ translationX, translationY }, context) => {
cx.value = context.x + translationX;
cy.value = context.y + translationY;
},
});

return (
<Touchable.Canvas style={styles.fill} panGesture={panGesture}>
<Touchable.Circle cx={cx} cy={cy} r={50} color="red" {...circleGesture} />
</Touchable.Canvas>
);

```

## Ingredients

### `Canvas`
Expand Down Expand Up @@ -111,7 +147,7 @@ return (
width={50}
height={50}
touchablePath={touchablePath}
{...circleGesture}
{...circleGesture}
/>
</Touchable.Canvas>
);
Expand Down
8 changes: 4 additions & 4 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
"web": "expo start --web"
},
"dependencies": {
"expo": "^49.0.13",
"expo-status-bar": "~1.6.0",
"expo": "^50.0.8",
"expo-status-bar": "~1.11.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.5",
"react-native": "0.73.4",
"react-native-web": "~0.19.6"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@expo/webpack-config": "^19.0.0",
"@expo/webpack-config": "~19.0.1",
"babel-loader": "^8.1.0",
"babel-plugin-module-resolver": "^4.1.0"
},
Expand Down
105 changes: 41 additions & 64 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import {
Blur,
Image,
rect,
rrect,
Selector,
Skia,
useComputedValue,
useImage,
useValue,
} from '@shopify/react-native-skia';
import { Blur, Image, rect, Skia, useImage } from '@shopify/react-native-skia';
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';

import Touchable, { useGestureHandler } from 'react-native-skia-gesture';

Expand All @@ -19,73 +11,52 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
const RADIUS = 80;
const RECT_WIDTH = 200;
const RECT_HEIGHT = 150;
const ROUNDED_RECT_RADIUS = 20;

const IMAGE_BACKGROUND =
'https://images.unsplash.com/photo-1530669244764-0909211cd8e8?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80';

export default function App() {
const cx = useValue(100);
const cy = useValue(400);
function App() {
const cx = useSharedValue(100);
const cy = useSharedValue(400);

const circleGesture = useGestureHandler<{ x: number; y: number }>({
onStart: (_, context) => {
context.x = cx.current;
context.y = cy.current;
'worklet';
context.x = cx.value;
context.y = cy.value;
},
onActive: ({ translationX, translationY }, context) => {
cx.current = context.x + translationX;
cy.current = context.y + translationY;
'worklet';
cx.value = context.x + translationX;
cy.value = context.y + translationY;
},
});

const rectX = useValue(100);
const rectY = useValue(100);
const rectX = useSharedValue(100);
const rectY = useSharedValue(100);

const rectGesture = useGestureHandler<{ x: number; y: number }>({
onStart: (_, context) => {
context.x = rectX.current;
context.y = rectY.current;
'worklet';
context.x = rectX.value;
context.y = rectY.value;
},
onActive: ({ translationX, translationY }, context) => {
rectX.current = context.x + translationX;
rectY.current = context.y + translationY;
'worklet';
rectX.value = context.x + translationX;
rectY.value = context.y + translationY;
},
});

const roundedRectLayout = useValue({
const roundedRectLayout = useSharedValue({
x: 150,
y: 300,
});

const roundedRectGesture = useGestureHandler<{ x: number; y: number }>({
onStart: (_, context) => {
context.x = roundedRectLayout.current.x;
context.y = roundedRectLayout.current.y;
},
onActive: ({ translationX, translationY }, context) => {
roundedRectLayout.current = {
x: context.x + translationX,
y: context.y + translationY,
};
},
});

const clipPath = useComputedValue(() => {
const clipPath = useDerivedValue(() => {
const path = Skia.Path.Make();
path.addCircle(cx.current, cy.current, RADIUS);
path.addRect(rect(rectX.current, rectY.current, RECT_WIDTH, RECT_HEIGHT));
path.addRRect(
rrect(
rect(
roundedRectLayout.current.x,
roundedRectLayout.current.y,
RECT_WIDTH,
RECT_HEIGHT
),
ROUNDED_RECT_RADIUS,
ROUNDED_RECT_RADIUS
)
);
path.addCircle(cx.value, cy.value, RADIUS);
path.addRect(rect(rectX.value, rectY.value, RECT_WIDTH, RECT_HEIGHT));
return path;
}, [cx, cy, rectX, rectY, roundedRectLayout]);

Expand All @@ -94,7 +65,12 @@ export default function App() {
if (!image) return <></>;

return (
<Touchable.Canvas style={styles.fill}>
<Touchable.Canvas
style={{
height: SCREEN_HEIGHT,
width: SCREEN_WIDTH,
}}
>
<Image
image={image}
fit="cover"
Expand Down Expand Up @@ -130,19 +106,20 @@ export default function App() {
color="rgba(255, 255, 255,0.2)"
{...rectGesture}
/>
<Touchable.RoundedRect
x={Selector(roundedRectLayout, (r) => r.x)}
y={Selector(roundedRectLayout, (r) => r.y)}
r={ROUNDED_RECT_RADIUS}
width={RECT_WIDTH}
height={RECT_HEIGHT}
color="rgba(255, 255, 255,0.2)"
{...roundedRectGesture}
/>
</Touchable.Canvas>
);
}

const AppContainer = () => {
return (
<GestureHandlerRootView style={styles.fill}>
<App />
</GestureHandlerRootView>
);
};

export default AppContainer;

const styles = StyleSheet.create({
fill: {
flex: 1,
Expand Down
Loading

0 comments on commit 7864259

Please sign in to comment.