Skip to content

Commit 43b6d39

Browse files
hannojgCopilot
andauthored
feat: add reanimated hook useSyncSharedValue & useDerivedValue (#306)
Added a reanimated example as well: https://github.com/user-attachments/assets/4537a5f9-3e82-4635-9edd-089d1ed1102c Main logic for this is to: ```tsx import { useSharedValue, withSequence, withSpring, withTiming } from 'react-native-reanimated' import { useSyncSharedValue, useDerivedValue } from 'react-native-filament' // Create a reanimated shared value to drive the animation const animatedRotationY = useSharedValue(0) // RNF works with rn-worklets-core (RNWC), so create a // RNWC shared value thats synced with the reanimated shared value const rotationY = useSyncSharedValue(animatedRotationY) // This uses useDerivedValue from rn-filament to create a RNWC derived value (RNWC has no API for that yet) const rotation = useDerivedValue<Float3>(() => { 'worklet' return [0, rotationY.value, 0] }) // Run a animation: const spin = useCallback(() => { animatedRotationY.value = withSequence(withSpring(Math.PI * 2), withTiming(0, { duration: 0 })) }, [animatedRotationY]) ``` --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2cc6e58 commit 43b6d39

13 files changed

Lines changed: 388 additions & 28 deletions

File tree

docs/docs/guides/GETTING_STARTED.mdx

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ sidebar_label: Getting Started
55
slug: /guides
66
---
77

8-
import Tabs from '@theme/Tabs'
9-
import TabItem from '@theme/TabItem'
8+
import Tabs from "@theme/Tabs";
9+
import TabItem from "@theme/TabItem";
1010

1111
## Installing the library
1212

@@ -45,7 +45,7 @@ For react-native-worklets-core its necessary to add a plugin to your `babel.conf
4545
</TabItem>
4646

4747
<TabItem value="w_rea">
48-
You should already use the reaniamted babel pluginVersions, make sure to add the `processNestedWorklets` option to it:
48+
You should already use the reanimated babel pluginVersions, make sure to add the `processNestedWorklets` option to it:
4949

5050
```js
5151
module.exports = {
@@ -60,16 +60,14 @@ For react-native-worklets-core its necessary to add a plugin to your `babel.conf
6060
</TabItem>
6161
</Tabs>
6262

63-
64-
6563
3. Update your pods:
6664

6765
```sh
6866
cd ios && pod install
6967
```
7068

7169
4. Start Metro with clean cache:
72-
70+
7371
```sh
7472
npm start -- --reset-cache
7573
```
@@ -83,9 +81,9 @@ The npm package of react-native-filament is quite huge, around ~400mb. That's be
8381
You'll likely import 3D related files when using react-native-filament. To make sure metro can resolve these files, you need to add the following to your `metro.config.js`:
8482

8583
```js
86-
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
84+
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
8785

88-
const defaultConfig = getDefaultConfig(__dirname)
86+
const defaultConfig = getDefaultConfig(__dirname);
8987

9088
/**
9189
* Metro configuration
@@ -94,10 +92,10 @@ const defaultConfig = getDefaultConfig(__dirname)
9492
* @type {import('metro-config').MetroConfig}
9593
*/
9694
const config = {
97-
resolver: {
98-
// This makes it possible to import .glb files in your code:
99-
assetExts: [...(defaultConfig.resolver?.assetExts || []), 'glb']
100-
}
95+
resolver: {
96+
// This makes it possible to import .glb files in your code:
97+
assetExts: [...(defaultConfig.resolver?.assetExts || []), "glb"],
98+
},
10199
};
102100

103101
module.exports = mergeConfig(defaultConfig, config);
@@ -117,25 +115,28 @@ For seeing some 3D content on the screen in your app, you need the following thi
117115
- 📹 A camera through which the scene is observed and projected onto the view (the `<Camera>` component)
118116
119117
```jsx
120-
import { FilamentScene, FilamentView, DefaultLight, Model, Camera } from "react-native-filament";
118+
import {
119+
FilamentScene,
120+
FilamentView,
121+
DefaultLight,
122+
Model,
123+
Camera,
124+
} from "react-native-filament";
121125
import MyModel from "./MyModel.glb";
122126

123127
function MyScene() {
124128
return (
125129
<FilamentScene>
126-
127130
{/* 🏞️ A view to draw the 3D content to */}
128131
<FilamentView style={{ flex: 1 }}>
132+
{/* 💡 A light source, otherwise the scene will be black */}
133+
<DefaultLight />
129134

130-
{/* 💡 A light source, otherwise the scene will be black */}
131-
<DefaultLight />
132-
133-
{/* 📦 A 3D model */}
134-
<Model source={MyModel} />
135-
136-
{/* 📹 A camera through which the scene is observed and projected onto the view */}
137-
<Camera />
135+
{/* 📦 A 3D model */}
136+
<Model source={MyModel} />
138137

138+
{/* 📹 A camera through which the scene is observed and projected onto the view */}
139+
<Camera />
139140
</FilamentView>
140141
</FilamentScene>
141142
);

docs/docs/guides/REANIMATED.mdx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
id: reanimated
3+
title: Reanimated Integration
4+
sidebar_label: Reanimated Integration
5+
slug: /guides/reanimated
6+
---
7+
8+
import useBaseUrl from "@docusaurus/useBaseUrl";
9+
10+
:::warning
11+
Please make sure to follow the installation steps to configure your metro config to work with reanimated, worklets-core and react-native-filament:
12+
13+
- [Installation Guide](./GETTING_STARTED.mdx)
14+
15+
:::
16+
17+
<a
18+
href="https://github.com/margelo/react-native-filament/blob/main/package/example/Shared/src/ReanimatedRotation.tsx"
19+
target="_blank"
20+
rel="noopener noreferrer"
21+
>
22+
<div class="image-container">
23+
<svg xmlns="http://www.w3.org/2000/svg" width="283" height="535">
24+
<image
25+
href={useBaseUrl("img/demo-reanimated.gif")}
26+
x="18"
27+
y="33"
28+
width="247"
29+
height="469"
30+
/>
31+
<image href={useBaseUrl("img/frame.png")} width="283" height="535" />
32+
</svg>
33+
</div>
34+
</a>
35+
36+
## react-native-worklets-core VS reanimated
37+
38+
react-native-filament is build with [`react-native-worklets-core`](https://npmjs.com/package/react-native-worklets-core) which is a library that provides shared values and worklets for React Native.
39+
It's API is very similar to reanimated's, but it is not reanimated!
40+
41+
In general, you pass shared values **from `react-native-worklets-core`** to the `react-native-filament` components to animate things like the position, rotation, scale, etc.
42+
43+
However, many times you want to create animations using reanimated's API like `withTiming`, `withSpring`, etc. and then pass the resulting shared values to the `react-native-filament` components.
44+
45+
## Using reanimated with react-native-filament
46+
47+
To use reanimated animations / shared values with react-native-filament, **you need to sync the shared values from reanimated to a shared value from `react-native-worklets-core`**.
48+
You can do that using the [`useSyncSharedValue`](../api/functions/useSyncSharedValue) hook:
49+
50+
```jsx
51+
import { useSyncSharedValue } from "react-native-filament";
52+
import { useSharedValue, withTiming } from "react-native-reanimated";
53+
54+
// Create a reanimated shared value to drive the animation
55+
const opacity = useSharedValue(0);
56+
57+
// Sync the reanimated shared value to a worklets-core shared value
58+
const workletsCoreOpacity = useSyncSharedValue(opacity);
59+
60+
// Drive the animation using reanimated
61+
opacity.value = withTiming(1, { duration: 1000 });
62+
63+
// The worklets-core shared value will have the same value as the
64+
// reanimated shared value and can be used in react-native-filament components
65+
```
66+
67+
### Using derived values
68+
69+
Sometimes you want to create a derived value from a reanimated shared value and use it in react-native-filament components. react-native-filament provides the [`useDerivedValue`](../api/functions/useDerivedValue) hook for that.
70+
It works like the `useDerivedValue` hook from reanimated, but just for worklets-core shared values:
71+
72+
```jsx
73+
import {
74+
useSharedValue,
75+
withSequence,
76+
withSpring,
77+
withTiming,
78+
} from "react-native-reanimated";
79+
import { useSyncSharedValue, useDerivedValue } from "react-native-filament";
80+
81+
// Create a reanimated shared value to drive the animation
82+
const animatedRotationY = useSharedValue(0);
83+
84+
// RNF works with rn-worklets-core (RNWC), so create a
85+
// RNWC shared value thats synced with the reanimated shared value
86+
const rotationY = useSyncSharedValue(animatedRotationY);
87+
88+
// This uses useDerivedValue from rn-filament to create a RNWC derived value (RNWC has no API for that yet)
89+
const rotation =
90+
useDerivedValue <
91+
Float3 >
92+
(() => {
93+
"worklet";
94+
return [0, rotationY.value, 0];
95+
});
96+
97+
// Run a animation:
98+
const spin = useCallback(() => {
99+
animatedRotationY.value = withSequence(
100+
withSpring(Math.PI * 2),
101+
withTiming(0, { duration: 0 })
102+
);
103+
}, [animatedRotationY]);
104+
```

docs/sidebars.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const sidebars: SidebarsConfig = {
1212
"guides/animator",
1313
"guides/images",
1414
"guides/physics",
15-
"guides/instancing"
15+
"guides/instancing",
16+
"guides/reanimated"
1617
],
1718
API: [
1819
"api/index",
2.65 MB
Loading

package/example/Shared/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ScaleEffect } from './ScaleEffect'
2020
import { ChangeMaterials } from './ChangeMaterials'
2121
import { SkyboxExample } from './SkyboxExample'
2222
import { MorphTargets } from './MorphTargets'
23+
import { ReanimatedRotation } from './ReanimatedRotation'
2324

2425
function NavigationItem(props: { name: string; route: string }) {
2526
const navigation = useNavigation()
@@ -59,6 +60,7 @@ function HomeScreen() {
5960
<NavigationItem name="🤖 Multiple Instances" route="MultipleInstances" />
6061
<NavigationItem name="🔄 Animated Rotate" route="AnimatedRotate" />
6162
<NavigationItem name="🔄 Animated Rotate w/ Shared Values" route="AnimatedRotateSharedValues" />
63+
<NavigationItem name="🐎 Reanimated Rotate" route="ReanimatedRotation" />
6264
<NavigationItem name="💰 Physics Coin" route="PhysicsCoin" />
6365
<NavigationItem name="😶‍🌫️ Fade Out" route="FadeOut" />
6466
<NavigationItem name="🌑 Cast Shadow" route="CastShadow" />
@@ -101,6 +103,7 @@ function App() {
101103
<Stack.Screen name="MultipleInstances" component={MultipleInstances} />
102104
<Stack.Screen name="AnimatedRotate" component={AnimatedRotate} />
103105
<Stack.Screen name="AnimatedRotateSharedValues" component={AnimatedRotateSharedValues} />
106+
<Stack.Screen name="ReanimatedRotation" component={ReanimatedRotation} />
104107
<Stack.Screen name="PhysicsCoin" component={PhysicsCoin} />
105108
<Stack.Screen name="FadeOut" component={FadeOut} />
106109
<Stack.Screen name="CastShadow" component={CastShadow} />
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react'
2+
import { Button, StyleSheet } from 'react-native'
3+
import {
4+
FilamentScene,
5+
FilamentView,
6+
Camera,
7+
Skybox,
8+
DefaultLight,
9+
Model,
10+
ModelInstance,
11+
Float3,
12+
useSyncSharedValue,
13+
useDerivedValue,
14+
} from 'react-native-filament'
15+
import DroneGlb from '~/assets/buster_drone.glb'
16+
import { useCallback } from 'react'
17+
import { useSharedValue, withSequence, withSpring, withTiming } from 'react-native-reanimated'
18+
19+
function Renderer() {
20+
const animatedRotationY = useSharedValue(0)
21+
const rotationY = useSyncSharedValue(animatedRotationY)
22+
const rotation = useDerivedValue<Float3>(() => {
23+
'worklet'
24+
return [0, rotationY.value, 0]
25+
})
26+
27+
const spin = useCallback(() => {
28+
animatedRotationY.value = withSequence(withSpring(Math.PI * 2), withTiming(0, { duration: 0 }))
29+
}, [animatedRotationY])
30+
31+
return (
32+
<>
33+
<Button title="Spin" onPress={spin} />
34+
<FilamentView style={styles.filamentView} enableTransparentRendering={false}>
35+
<Camera />
36+
<DefaultLight />
37+
<Skybox colorInHex="#88defb" />
38+
39+
<Model source={DroneGlb} transformToUnitCube scale={[3, 3, 3]}>
40+
{/* Note: we apply the rotation individually as the above transformations are multiplying, while the one for the rotation, shouldn't */}
41+
<ModelInstance index={0} rotate={rotation} multiplyWithCurrentTransform={false} />
42+
</Model>
43+
</FilamentView>
44+
</>
45+
)
46+
}
47+
48+
export function ReanimatedRotation() {
49+
return (
50+
<FilamentScene>
51+
<Renderer />
52+
</FilamentScene>
53+
)
54+
}
55+
56+
const styles = StyleSheet.create({
57+
container: {
58+
flex: 1,
59+
},
60+
filamentView: {
61+
flex: 1,
62+
},
63+
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copied from https://github.com/mrousavy/react-native-vision-camera/blob/main/package/src/dependencies/ModuleProxy.ts
2+
3+
type ImportType = ReturnType<typeof require>
4+
5+
/**
6+
* Create a lazily-imported module proxy.
7+
* This is useful for lazily requiring optional dependencies.
8+
*/
9+
export const createModuleProxy = <TModule>(getModule: () => ImportType): TModule => {
10+
const holder: { module: TModule | undefined } = { module: undefined }
11+
12+
const proxy = new Proxy(holder, {
13+
get: (target, property) => {
14+
if (property === '$$typeof') {
15+
// If inlineRequires is enabled, Metro will look up all imports
16+
// with the $$typeof operator. In this case, this will throw the
17+
// `OptionalDependencyNotInstalledError` error because we try to access the module
18+
// even though we are not using it (Metro does it), so instead we return undefined
19+
// to bail out of inlineRequires here.
20+
// See https://github.com/mrousavy/react-native-vision-camera/pull/2953
21+
return undefined
22+
}
23+
24+
if (target.module == null) {
25+
// lazy initialize module via require()
26+
// caller needs to make sure the require() call is wrapped in a try/catch
27+
target.module = getModule() as TModule
28+
}
29+
return target.module[property as keyof typeof holder.module]
30+
},
31+
})
32+
return proxy as unknown as TModule
33+
}
34+
35+
export class OptionalDependencyNotInstalledError extends Error {
36+
constructor(name: string) {
37+
super(`${name} is not installed!`)
38+
}
39+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copied from https://github.com/mrousavy/react-native-vision-camera/blob/main/package/src/dependencies/ReanimatedProxy.ts
2+
3+
import type * as Reanimated from 'react-native-reanimated'
4+
import { createModuleProxy, OptionalDependencyNotInstalledError } from './ModuleProxy'
5+
6+
type TReanimated = typeof Reanimated
7+
8+
/**
9+
* A proxy object that lazy-imports react-native-reanimated as soon as the
10+
* caller tries to access a property on {@linkcode ReanimatedProxy}.
11+
*
12+
* If react-native-reanimated is not installed, accessing anything on
13+
* {@linkcode ReanimatedProxy} will throw.
14+
*/
15+
export const ReanimatedProxy = createModuleProxy<TReanimated>(() => {
16+
try {
17+
return require('react-native-reanimated')
18+
} catch (e) {
19+
throw new OptionalDependencyNotInstalledError('react-native-reanimated')
20+
}
21+
})

0 commit comments

Comments
 (0)