diff --git a/RNRive.podspec b/RNRive.podspec index 99664968..3b896068 100644 --- a/RNRive.podspec +++ b/RNRive.podspec @@ -41,13 +41,20 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/rive-app/rive-nitro-react-native.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,swift}" + s.source_files = "ios/**/*.{h,m,mm,swift}", "cpp/**/*.{hpp,cpp}" s.public_header_files = ['ios/RCTSwiftLog.h'] + s.private_header_files = ['cpp/**/*.hpp'] + + # Set pod_target_xcconfig BEFORE add_nitrogen_files so it gets merged with Nitro's settings + s.pod_target_xcconfig = { + 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"' + } + load 'nitrogen/generated/ios/RNRive+autolinking.rb' add_nitrogen_files(s) s.dependency "RiveRuntime", rive_ios_version - install_modules_dependencies(s) + install_modules_dependencies(s) end diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 038c7a6e..8bbf93e1 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -6,7 +6,10 @@ set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_CXX_STANDARD 20) # Define C++ library and add all sources -add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) +add_library(${PACKAGE_NAME} SHARED + src/main/cpp/cpp-adapter.cpp + src/main/cpp/JRiveWorkletDispatcher.cpp +) # Add Nitrogen specs :) include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/rive+autolinking.cmake) diff --git a/android/src/main/cpp/JRiveWorkletDispatcher.cpp b/android/src/main/cpp/JRiveWorkletDispatcher.cpp new file mode 100644 index 00000000..f0745bfa --- /dev/null +++ b/android/src/main/cpp/JRiveWorkletDispatcher.cpp @@ -0,0 +1,81 @@ +#include "JRiveWorkletDispatcher.hpp" +#include + +namespace margelo::nitro::rive { + +using namespace facebook; + +JRiveWorkletDispatcher::JRiveWorkletDispatcher( + jni::alias_ref jThis) + : _javaPart(jni::make_global(jThis)) {} + +jni::local_ref JRiveWorkletDispatcher::initHybrid( + jni::alias_ref jThis) { + return makeCxxInstance(jThis); +} + +jni::local_ref JRiveWorkletDispatcher::create() { + return newObjectJavaArgs(); +} + +void JRiveWorkletDispatcher::trigger() { + std::unique_lock lock(_mutex); + while (!_jobs.empty()) { + auto job = std::move(_jobs.front()); + _jobs.pop(); + lock.unlock(); + job(); + lock.lock(); + } +} + +void JRiveWorkletDispatcher::scheduleTrigger() { + static const auto method = _javaPart->getClass()->getMethod("scheduleTrigger"); + method(_javaPart.get()); +} + +void JRiveWorkletDispatcher::runAsync(std::function&& function) { + std::unique_lock lock(_mutex); + _jobs.push(std::move(function)); + lock.unlock(); + scheduleTrigger(); +} + +void JRiveWorkletDispatcher::runSync(std::function&& function) { + std::mutex mtx; + std::condition_variable cv; + bool done = false; + + runAsync([&]() { + function(); + { + std::lock_guard lock(mtx); + done = true; + } + cv.notify_one(); + }); + + std::unique_lock lock(mtx); + cv.wait(lock, [&]{ return done; }); +} + +void JRiveWorkletDispatcher::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JRiveWorkletDispatcher::initHybrid), + makeNativeMethod("trigger", JRiveWorkletDispatcher::trigger), + }); +} + +AndroidMainThreadDispatcher::AndroidMainThreadDispatcher( + jni::local_ref javaDispatcher) + : _javaDispatcher(jni::make_global(javaDispatcher)) {} + +void AndroidMainThreadDispatcher::runAsync(std::function&& function) { + _javaDispatcher->cthis()->runAsync(std::move(function)); +} + +void AndroidMainThreadDispatcher::runSync(std::function&& function) { + _javaDispatcher->cthis()->runSync(std::move(function)); +} + +} // namespace margelo::nitro::rive diff --git a/android/src/main/cpp/JRiveWorkletDispatcher.hpp b/android/src/main/cpp/JRiveWorkletDispatcher.hpp new file mode 100644 index 00000000..280d1dd4 --- /dev/null +++ b/android/src/main/cpp/JRiveWorkletDispatcher.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace margelo::nitro::rive { + +using namespace facebook; + +class JRiveWorkletDispatcher : public jni::HybridClass { +public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/RiveWorkletDispatcher;"; + + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + static jni::local_ref create(); + + void runAsync(std::function&& function); + void runSync(std::function&& function); + +private: + friend HybridBase; + + void trigger(); + void scheduleTrigger(); + + jni::global_ref _javaPart; + std::queue> _jobs; + std::recursive_mutex _mutex; + + explicit JRiveWorkletDispatcher(jni::alias_ref jThis); +}; + +class AndroidMainThreadDispatcher : public Dispatcher { +public: + explicit AndroidMainThreadDispatcher(jni::local_ref javaDispatcher); + + void runAsync(std::function&& function) override; + void runSync(std::function&& function) override; + +private: + jni::global_ref _javaDispatcher; +}; + +} // namespace margelo::nitro::rive diff --git a/android/src/main/cpp/cpp-adapter.cpp b/android/src/main/cpp/cpp-adapter.cpp index 5116d53c..4c5af328 100644 --- a/android/src/main/cpp/cpp-adapter.cpp +++ b/android/src/main/cpp/cpp-adapter.cpp @@ -1,6 +1,9 @@ #include #include "riveOnLoad.hpp" +#include "JRiveWorkletDispatcher.hpp" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { - return margelo::nitro::rive::initialize(vm); + auto result = margelo::nitro::rive::initialize(vm); + margelo::nitro::rive::JRiveWorkletDispatcher::registerNatives(); + return result; } diff --git a/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt b/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt new file mode 100644 index 00000000..d6f08688 --- /dev/null +++ b/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt @@ -0,0 +1,49 @@ +package com.margelo.nitro.rive + +import android.os.Handler +import android.os.Looper +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import java.util.concurrent.atomic.AtomicBoolean + +@Suppress("JavaJniMissingFunction") +@Keep +@DoNotStrip +class RiveWorkletDispatcher { + @DoNotStrip + @Suppress("unused") + private val mHybridData: HybridData = initHybrid() + + private val mainHandler = Handler(Looper.getMainLooper()) + private val active = AtomicBoolean(true) + + private val triggerRunnable = Runnable { + synchronized(active) { + if (active.get()) { + trigger() + } + } + } + + private external fun initHybrid(): HybridData + private external fun trigger() + + @DoNotStrip + @Suppress("unused") + private fun scheduleTrigger() { + mainHandler.post(triggerRunnable) + } + + fun deactivate() { + synchronized(active) { + active.set(false) + } + } + + companion object { + init { + System.loadLibrary("rive") + } + } +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..d7bba5a4 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,33 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + +comment: + layout: "header, diff, flags, components" + behavior: default + require_changes: true + +flags: + unit: + paths: + - src/ + carryforward: true + harness: + paths: + - src/ + carryforward: true + +ignore: + - "**/*.nitro.ts" + - "**/index.ts" + - "**/index.tsx" + - "nitrogen/**" + - "example/**" + - "expo-example/**" + - "lib/**" diff --git a/cpp/HybridRiveWorkletBridge.hpp b/cpp/HybridRiveWorkletBridge.hpp new file mode 100644 index 00000000..f58d8cc2 --- /dev/null +++ b/cpp/HybridRiveWorkletBridge.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "HybridRiveWorkletBridgeSpec.hpp" +#include + +#if __APPLE__ +#include +#include +#elif __ANDROID__ +#include "JRiveWorkletDispatcher.hpp" +#endif + +namespace margelo::nitro::rive { + +#if __APPLE__ + +/** + * iOS: A dispatcher that runs work on the main thread using GCD. + */ +class MainThreadDispatcher : public Dispatcher { +public: + void runAsync(std::function&& function) override { + __block auto func = std::move(function); + dispatch_async(dispatch_get_main_queue(), ^{ + func(); + }); + } + + void runSync(std::function&& function) override { + if (pthread_main_np() != 0) { + function(); + } else { + __block auto func = std::move(function); + dispatch_sync(dispatch_get_main_queue(), ^{ + func(); + }); + } + } +}; + +#endif + +class HybridRiveWorkletBridge : public HybridRiveWorkletBridgeSpec { +public: + HybridRiveWorkletBridge() : HybridObject(TAG) {} + + void install() override { + throw std::runtime_error("install() requires runtime access - use raw method"); + } + +protected: + void loadHybridMethods() override { + HybridObject::loadHybridMethods(); + registerHybrids(this, [](Prototype& prototype) { + prototype.registerRawHybridMethod("install", 0, &HybridRiveWorkletBridge::installRaw); + }); + } + +private: + jsi::Value installRaw(jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* args, + size_t count) { +#if __APPLE__ + auto dispatcher = std::make_shared(); + Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher); +#elif __ANDROID__ + // Create the Java dispatcher instance and wrap it in the C++ dispatcher + auto javaDispatcher = JRiveWorkletDispatcher::create(); + auto dispatcher = std::make_shared(javaDispatcher); + Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher); +#endif + return jsi::Value::undefined(); + } +}; + +} // namespace margelo::nitro::rive diff --git a/example/__tests__/hooks.harness.tsx b/example/__tests__/hooks.harness.tsx index 7972db4a..0c32e363 100644 --- a/example/__tests__/hooks.harness.tsx +++ b/example/__tests__/hooks.harness.tsx @@ -16,7 +16,6 @@ import { } from '@rive-app/react-native'; import type { ViewModelInstance } from '@rive-app/react-native'; -const QUICK_START = require('../assets/rive/quick_start.riv'); const DATABINDING = require('../assets/rive/databinding.riv'); type UseRiveNumberContext = { @@ -45,7 +44,7 @@ function UseRiveNumberTestComponent({ instance: ViewModelInstance; context: UseRiveNumberContext; }) { - const { value, setValue, error } = useRiveNumber('health', instance); + const { value, setValue, error } = useRiveNumber('age', instance); useEffect(() => { context.value = value; @@ -94,54 +93,58 @@ function expectDefined(value: T): asserts value is NonNullable { describe('useRiveNumber Hook', () => { it('returns value from number property', async () => { - const file = await RiveFileFactory.fromSource(QUICK_START, undefined); - const vm = file.defaultArtboardViewModel(); + const file = await RiveFileFactory.fromSource(DATABINDING, undefined); + const vm = file.viewModelByName('Person'); expectDefined(vm); - const instance = vm.createDefaultInstance(); + const instance = vm.createInstanceByName('Gordon'); expectDefined(instance); const context = createUseRiveNumberContext(); - await render( - - ); - - await waitFor( - () => { - expect(context.error).toBeNull(); - expect(typeof context.value).toBe('number'); - }, - { timeout: 5000 } - ); - - cleanup(); + try { + await render( + + ); + + await waitFor( + () => { + expect(context.error).toBeNull(); + expect(typeof context.value).toBe('number'); + }, + { timeout: 5000 } + ); + } finally { + cleanup(); + } }); it('can set value via setValue', async () => { - const file = await RiveFileFactory.fromSource(QUICK_START, undefined); - const vm = file.defaultArtboardViewModel(); + const file = await RiveFileFactory.fromSource(DATABINDING, undefined); + const vm = file.viewModelByName('Person'); expectDefined(vm); - const instance = vm.createDefaultInstance(); + const instance = vm.createInstanceByName('Gordon'); expectDefined(instance); const context = createUseRiveNumberContext(); - await render( - - ); - - await waitFor( - () => { - expect(context.setValue).not.toBeNull(); - }, - { timeout: 5000 } - ); - - context.setValue!(42); - - const property = instance.numberProperty('health'); - expectDefined(property); - expect(property.value).toBe(42); - - cleanup(); + try { + await render( + + ); + + await waitFor( + () => { + expect(context.setValue).not.toBeNull(); + }, + { timeout: 5000 } + ); + + context.setValue!(42); + + const property = instance.numberProperty('age'); + expectDefined(property); + expect(property.value).toBe(42); + } finally { + cleanup(); + } }); }); diff --git a/example/__tests__/worklet-bridge.harness.tsx b/example/__tests__/worklet-bridge.harness.tsx new file mode 100644 index 00000000..0489a8a3 --- /dev/null +++ b/example/__tests__/worklet-bridge.harness.tsx @@ -0,0 +1,216 @@ +import { + describe, + it, + expect, + render, + waitFor, + cleanup, +} from 'react-native-harness'; +import { View } from 'react-native'; +import { useEffect, useMemo } from 'react'; +import { NitroModules } from 'react-native-nitro-modules'; +import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets'; + +import { + Fit, + RiveView, + RiveFileFactory, + useRiveFile, + useViewModelInstance, +} from '@rive-app/react-native'; +import type { RiveWorkletBridge } from '@rive-app/react-native'; + +const DATABINDING = require('../assets/rive/databinding.riv'); +const BOUNCING_BALL = require('../assets/rive/bouncing_ball.riv'); + +function expectDefined(value: T): asserts value is NonNullable { + expect(value).toBeDefined(); +} + +// Note: installWorkletDispatcher is already called in App.tsx at startup. +// UI thread listeners are designed for Rive-driven value changes (animation/data binding), +// not for JS-thread value changes. Testing the full UI thread listener flow requires +// a Rive file with animation that drives property changes. + +describe('Worklet Bridge', () => { + it('RiveWorkletBridge HybridObject can be created', () => { + const bridge = + NitroModules.createHybridObject('RiveWorkletBridge'); + expect(bridge).toBeDefined(); + }); + + it('property can be boxed for worklet use', async () => { + const file = await RiveFileFactory.fromSource(DATABINDING, undefined); + const vm = file.viewModelByName('Person'); + expectDefined(vm); + const instance = vm.createInstanceByName('Gordon'); + expectDefined(instance); + + const property = instance.numberProperty('age'); + expectDefined(property); + + // Verify boxing works (required for passing to worklets) + const boxedProperty = NitroModules.box(property); + expect(boxedProperty).toBeDefined(); + + // Verify the property value can be read + expect(property.value).toBe(30); // Gordon's age is 30 + }); + + // TODO: for some reason those wont run in harness environment + if (!global.RN_HARNESS) { + it('JS thread listener is called when Rive animation changes value', async () => { + // Listeners are notified when Rive animation/data binding changes values. + // This test uses bouncing_ball.riv which has an animation that drives ypos. + + let receivedValue: number | undefined; + + function ListenerTestComponent({ + onResult, + }: { + onResult: (value: number) => void; + }) { + const { riveFile } = useRiveFile(BOUNCING_BALL); + const instance = useViewModelInstance(riveFile); + + const property = useMemo( + () => instance?.numberProperty('ypos'), + [instance] + ); + + useEffect( + () => + property?.addListener((value) => { + onResult(value); + }), + [property, onResult] + ); + + if (!riveFile || !instance) { + return ; + } + + return ( + + ); + } + + try { + await render( + { + receivedValue = value; + }} + /> + ); + + await waitFor( + () => { + expect(receivedValue).toBeDefined(); + expect(typeof receivedValue).toBe('number'); + }, + { timeout: 5000 } + ); + } finally { + cleanup(); + } + }); + + it('UI thread listener is called when Rive animation changes value', async () => { + // Same as above but listener is registered via scheduleOnUI, so callback runs on UI thread + + type ListenerResult = { + calledOnUIThread: boolean; + receivedValue: number; + }; + + let result: ListenerResult | undefined; + + function UIThreadListenerTestComponent({ + onResult, + }: { + onResult: (r: ListenerResult) => void; + }) { + const { riveFile } = useRiveFile(BOUNCING_BALL); + const instance = useViewModelInstance(riveFile); + + const property = useMemo( + () => instance?.numberProperty('ypos'), + [instance] + ); + + useEffect(() => { + if (!property) return; + + const boxedProperty = NitroModules.box(property); + + const reportResult = (r: ListenerResult) => { + onResult(r); + }; + + scheduleOnUI(() => { + 'worklet'; + const prop = boxedProperty.unbox(); + prop.addListener((value: number) => { + 'worklet'; + // Check if we're on UI thread + const isOnUIThread = + typeof global._WORKLET !== 'undefined' && + global._WORKLET === true; + scheduleOnRN(reportResult, { + calledOnUIThread: isOnUIThread, + receivedValue: value, + }); + }); + }); + + return () => { + property.removeListeners(); + }; + }, [property, onResult]); + + if (!riveFile || !instance) { + return ; + } + + return ( + + ); + } + + try { + await render( + { + result = r; + }} + /> + ); + + // Wait for animation to trigger the listener (bouncing ball should change ypos quickly) + await waitFor( + () => { + expect(result).toBeDefined(); + expect(result!.calledOnUIThread).toBe(true); + expect(typeof result!.receivedValue).toBe('number'); + }, + { timeout: 5000 } + ); + } finally { + cleanup(); + } + }); + } +}); diff --git a/example/assets/rive/bouncing_ball.riv b/example/assets/rive/bouncing_ball.riv new file mode 100644 index 00000000..d83155fa Binary files /dev/null and b/example/assets/rive/bouncing_ball.riv differ diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b67556ef..9e618dcf 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1904,7 +1904,7 @@ PODS: - ReactCommon/turbomodule/core - RNWorklets - Yoga - - RNRive (0.1.5): + - RNRive (0.2.0): - DoubleConversion - glog - hermes-engine @@ -2330,7 +2330,7 @@ SPEC CHECKSUMS: RNCPicker: 83c74db2de8274d8a8f3e18d91dea174a708f8c4 RNGestureHandler: bff91bb5ab5688265c70f74180ef718b94f33fe3 RNReanimated: 9a24892f34ea317264883806d2e3de7ce34eab90 - RNRive: 73aa1ec7d3ef4da1030e81643808adb538bc05ca + RNRive: 509163d7fb4dcced11210670f0f6e7cb2f904e30 RNWorklets: ddf16938b1ed7e878563a4fc8a690968ef3d27f1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f diff --git a/example/src/App.tsx b/example/src/App.tsx index 0d193a0a..28913e9a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -11,11 +11,16 @@ import { import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { runOnUI } from 'react-native-reanimated'; +import { installWorkletDispatcher } from '@rive-app/react-native'; import { PagesList, type PageItem } from './PagesList'; import { HomeMenu } from './shared/HomeMenu'; const LAST_OPENED_KEY = '@rive_example_last_opened'; +// Install dispatcher on Reanimated's UI runtime for worklet-based listeners +installWorkletDispatcher(runOnUI); + type RootStackParamList = { Home: undefined; } & { diff --git a/example/src/exercisers/RiveToReactNativeExample.tsx b/example/src/exercisers/RiveToReactNativeExample.tsx new file mode 100644 index 00000000..4d90152e --- /dev/null +++ b/example/src/exercisers/RiveToReactNativeExample.tsx @@ -0,0 +1,332 @@ +import { + View, + Text, + StyleSheet, + ActivityIndicator, + Pressable, + Switch, +} from 'react-native'; +import { useEffect, useMemo, useState } from 'react'; +import Animated, { + runOnUI, + useSharedValue, + useAnimatedStyle, + type SharedValue, +} from 'react-native-reanimated'; +import { NitroModules } from 'react-native-nitro-modules'; +import { + Fit, + RiveView, + useRiveFile, + type RiveFile, + type ViewModelInstance, + type ViewModelNumberProperty, +} from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +declare global { + var __callMicrotasks: () => void; +} + +/** + * Syncs a Rive ViewModel number property to a Reanimated SharedValue. + * @param useUIThread - If true, runs listener on UI thread (won't freeze when JS blocked). + * If false, runs on JS thread (will freeze when JS blocked). + */ +function useRiveNumberListener( + property: ViewModelNumberProperty | undefined, + sharedValue: SharedValue, + useUIThread: boolean +) { + useEffect(() => { + if (!property) return; + + if (useUIThread) { + // UI thread version - won't freeze when JS thread is blocked + const boxedProperty = NitroModules.box(property); + const sv = sharedValue; + + runOnUI(() => { + 'worklet'; + const prop = boxedProperty.unbox(); + prop.addListener((value: number) => { + 'worklet'; + sv.value = value; + global.__callMicrotasks(); + }); + })(); + + return () => { + property.removeListeners(); + }; + } else { + // JS thread version - will freeze when JS thread is blocked + const removeListener = property.addListener((value: number) => { + sharedValue.value = value; + }); + + return removeListener; + } + }, [property, sharedValue, useUIThread]); +} + +export default function RiveToReactNativeExample() { + const { riveFile, isLoading, error } = useRiveFile( + require('../../assets/rive/bouncing_ball.riv') + ); + + return ( + + {isLoading ? ( + + ) : riveFile ? ( + + ) : ( + {error || 'Unexpected error'} + )} + + ); +} + +function WithViewModelSetup({ file }: { file: RiveFile }) { + const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]); + const instance = useMemo( + () => viewModel?.createDefaultInstance(), + [viewModel] + ); + const [useUIThread, setUseUIThread] = useState(true); + + if (!instance || !viewModel) { + return ( + + + {!viewModel + ? 'No view model found.' + : 'Failed to create view model instance'} + + + This demo requires a Rive file (bouncing_ball.riv) with:{'\n'} + {'\n'}• A ViewModel with a "ypos" number property{'\n'}• A bouncing + ball animation{'\n'}• Target-to-source binding from ball Y position to + ypos{'\n'} + {'\n'} + See Rive docs for data binding setup. + + + ); + } + + return ( + + ); +} + +function BouncingBallTracker({ + instance, + file, + useUIThread, + onToggle, +}: { + instance: ViewModelInstance; + file: RiveFile; + useUIThread: boolean; + onToggle: (value: boolean) => void; +}) { + const pointerY = useSharedValue(0); + + const yposProperty = useMemo( + () => instance.numberProperty('ypos'), + [instance] + ); + + useRiveNumberListener(yposProperty, pointerY, useUIThread); + + const pointerStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: pointerY.value }], + })); + + if (!yposProperty) { + return ( + + Property "ypos" not found + + Make sure the Rive file has a "ypos" number property in its ViewModel + with target-to-source binding from the ball's Y position. + + + ); + } + + return ( + + + Rive drives the ball position via data binding.{'\n'}React Native tracks + it with the blue pointer using addListener. + + + + JS Thread + + UI Thread + + + + + + + RN + + + + + + ); +} + +function BlockJSThreadButton() { + const [isBlocking, setIsBlocking] = useState(false); + + const handlePress = () => { + setIsBlocking(true); + + // Use setTimeout to let the state update render before blocking + setTimeout(() => { + const start = Date.now(); + while (Date.now() - start < 2000) { + // Busy poll - blocks JS thread for 2 seconds + } + setIsBlocking(false); + }, 50); + }; + + return ( + + + {isBlocking ? 'JS Thread Blocked...' : 'Block JS Thread (2s)'} + + + ); +} + +RiveToReactNativeExample.metadata = { + name: 'Rive → React Native', + description: + 'Demonstrates Rive graphics driving React Native UI through data binding listeners', + riveMarketplaceLink: + 'https://rive.app/community/files/25997-48571-demo-for-tracking-rive-property-in-react-native/', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + subtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginVertical: 10, + paddingHorizontal: 20, + }, + switchContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 10, + marginBottom: 10, + }, + switchLabel: { + fontSize: 14, + color: '#333', + }, + contentContainer: { + position: 'relative', + height: 600, + width: 200, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: '#ccc', + }, + rive: { + width: 100, + height: 600, + }, + pointer: { + position: 'absolute', + top: -10, + right: 40, + flexDirection: 'row', + alignItems: 'center', + }, + pointerArrow: { + width: 0, + height: 0, + borderTopWidth: 10, + borderBottomWidth: 10, + borderRightWidth: 15, + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + borderRightColor: '#007AFF', + }, + pointerText: { + backgroundColor: '#007AFF', + color: '#fff', + fontSize: 12, + fontWeight: 'bold', + paddingHorizontal: 6, + paddingVertical: 4, + borderTopRightRadius: 4, + borderBottomRightRadius: 4, + }, + errorText: { + color: 'red', + textAlign: 'center', + fontSize: 16, + fontWeight: 'bold', + marginBottom: 10, + }, + instructionText: { + color: '#666', + textAlign: 'left', + fontSize: 14, + lineHeight: 22, + }, + blockButton: { + backgroundColor: '#4CAF50', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + marginTop: 20, + alignSelf: 'center', + }, + blockButtonActive: { + backgroundColor: '#f44336', + }, + blockButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, +}); diff --git a/example/src/shared/metadata.ts b/example/src/shared/metadata.ts index f0b0154b..f35468bc 100644 --- a/example/src/shared/metadata.ts +++ b/example/src/shared/metadata.ts @@ -2,4 +2,5 @@ export type Metadata = { name: string; description: string; order?: number; // Lower numbers appear first, undefined = end of list + riveMarketplaceLink?: string; }; diff --git a/example/src/tests/TestsPage.tsx b/example/src/tests/TestsPage.tsx index e1b3a078..3de819bb 100644 --- a/example/src/tests/TestsPage.tsx +++ b/example/src/tests/TestsPage.tsx @@ -7,6 +7,8 @@ import { ActivityIndicator, } from 'react-native'; import { getTestCollector } from 'react-native-harness'; +// @ts-expect-error - internal module not exported +import { TestComponentOverlay } from '@react-native-harness/runtime/dist/render/TestComponentOverlay'; import type { TestSuite, TestCase } from '@react-native-harness/bridge'; import { useState, useEffect } from 'react'; import type { Metadata } from '../shared/metadata'; @@ -85,10 +87,11 @@ export default function TestsPage() { await test.fn(); setTestStates((prev) => new Map(prev).set(key, { status: 'passed' })); } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); setTestStates((prev) => new Map(prev).set(key, { status: 'failed', - error: e instanceof Error ? e.message : String(e), + error: errorMessage, }) ); } @@ -202,6 +205,7 @@ export default function TestsPage() { ))} + ); } diff --git a/expo-example/app/_layout.tsx b/expo-example/app/_layout.tsx index d10c2b6e..752b4568 100644 --- a/expo-example/app/_layout.tsx +++ b/expo-example/app/_layout.tsx @@ -5,9 +5,14 @@ import { } from '@react-navigation/native'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; +import { runOnUI } from 'react-native-reanimated'; +import { installWorkletDispatcher } from '@rive-app/react-native'; import { useColorScheme } from '@/hooks/use-color-scheme'; +// Install dispatcher on Reanimated's UI runtime for worklet-based listeners +installWorkletDispatcher(runOnUI); + export default function RootLayout() { const colorScheme = useColorScheme(); diff --git a/expo-example/assets/rive/bouncing_ball.riv b/expo-example/assets/rive/bouncing_ball.riv new file mode 100644 index 00000000..d83155fa Binary files /dev/null and b/expo-example/assets/rive/bouncing_ball.riv differ diff --git a/expo-example/metro.config.js b/expo-example/metro.config.js index 0870b384..a3ae10fc 100644 --- a/expo-example/metro.config.js +++ b/expo-example/metro.config.js @@ -15,6 +15,26 @@ const bobConfig = getConfig(config, { project: __dirname, }); +// Block resolution from example/node_modules to avoid version conflicts +// (expo-example uses different reanimated/worklets versions than example app) +const escapeRegex = (str) => str.replace(/[/\\]/g, '[/\\\\]'); +const exampleNodeModules = path.join(root, 'example', 'node_modules'); +const blockPatterns = [ + new RegExp( + escapeRegex(path.join(exampleNodeModules, 'react-native-reanimated')) + '.*' + ), + new RegExp( + escapeRegex(path.join(exampleNodeModules, 'react-native-worklets')) + '.*' + ), +]; +const existingBlockList = bobConfig.resolver.blockList; +if (existingBlockList) { + blockPatterns.push(existingBlockList); +} +bobConfig.resolver.blockList = new RegExp( + blockPatterns.map((r) => r.source).join('|') +); + /** * Resolves @example/* path aliases to the example/src/* directory. * Metro doesn't natively understand TypeScript path mappings, so this diff --git a/expo-example/package.json b/expo-example/package.json index 53894daf..6ba4310f 100644 --- a/expo-example/package.json +++ b/expo-example/package.json @@ -36,11 +36,11 @@ "react-native": "0.81.5", "react-native-gesture-handler": "2.29.1", "react-native-nitro-modules": "0.33.2", - "react-native-reanimated": "4.1.5", + "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", - "react-native-worklets": "0.6.1" + "react-native-worklets": "0.7.1" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/nitro.json b/nitro.json index 2213016c..c0bf4f1d 100644 --- a/nitro.json +++ b/nitro.json @@ -28,6 +28,9 @@ "RiveImageFactory": { "swift": "HybridRiveImageFactory", "kotlin": "HybridRiveImageFactory" + }, + "RiveWorkletBridge": { + "cpp": "HybridRiveWorkletBridge" } }, "ignorePaths": ["node_modules"] diff --git a/nitrogen/generated/android/rive+autolinking.cmake b/nitrogen/generated/android/rive+autolinking.cmake index a613c129..273be54b 100644 --- a/nitrogen/generated/android/rive+autolinking.cmake +++ b/nitrogen/generated/android/rive+autolinking.cmake @@ -41,6 +41,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridRiveImageFactorySpec.cpp ../nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp ../nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp + ../nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelPropertySpec.cpp diff --git a/nitrogen/generated/android/riveOnLoad.cpp b/nitrogen/generated/android/riveOnLoad.cpp index 8ffe3fd1..11f0f890 100644 --- a/nitrogen/generated/android/riveOnLoad.cpp +++ b/nitrogen/generated/android/riveOnLoad.cpp @@ -42,6 +42,7 @@ #include "JHybridViewModelListPropertySpec.hpp" #include "JHybridViewModelArtboardPropertySpec.hpp" #include +#include "HybridRiveWorkletBridge.hpp" namespace margelo::nitro::rive { @@ -120,6 +121,15 @@ int initialize(JavaVM* vm) { return instance->cthis()->shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RiveWorkletBridge", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRiveWorkletBridge\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); }); } diff --git a/nitrogen/generated/ios/RNRiveAutolinking.mm b/nitrogen/generated/ios/RNRiveAutolinking.mm index b606789c..daf807b5 100644 --- a/nitrogen/generated/ios/RNRiveAutolinking.mm +++ b/nitrogen/generated/ios/RNRiveAutolinking.mm @@ -15,6 +15,7 @@ #include "HybridRiveFileSpecSwift.hpp" #include "HybridRiveViewSpecSwift.hpp" #include "HybridRiveImageFactorySpecSwift.hpp" +#include "HybridRiveWorkletBridge.hpp" @interface RNRiveAutolinking : NSObject @end @@ -60,6 +61,15 @@ + (void) load { return hybridObject; } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RiveWorkletBridge", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRiveWorkletBridge\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); } @end diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp new file mode 100644 index 00000000..aa30c195 --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridRiveWorkletBridgeSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridRiveWorkletBridgeSpec.hpp" + +namespace margelo::nitro::rive { + + void HybridRiveWorkletBridgeSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("install", &HybridRiveWorkletBridgeSpec::install); + }); + } + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp new file mode 100644 index 00000000..7946695d --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp @@ -0,0 +1,62 @@ +/// +/// HybridRiveWorkletBridgeSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + + + +namespace margelo::nitro::rive { + + using namespace margelo::nitro; + + /** + * An abstract base class for `RiveWorkletBridge` + * Inherit this class to create instances of `HybridRiveWorkletBridgeSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridRiveWorkletBridge: public HybridRiveWorkletBridgeSpec { + * public: + * HybridRiveWorkletBridge(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridRiveWorkletBridgeSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridRiveWorkletBridgeSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridRiveWorkletBridgeSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual void install() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "RiveWorkletBridge"; + }; + +} // namespace margelo::nitro::rive diff --git a/src/core/WorkletBridge.ts b/src/core/WorkletBridge.ts new file mode 100644 index 00000000..9bf82706 --- /dev/null +++ b/src/core/WorkletBridge.ts @@ -0,0 +1,47 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { RiveWorkletBridge } from '../specs/RiveWorkletBridge.nitro'; + +let isInstalled = false; + +/** + * Install the Nitro Dispatcher on Reanimated's UI runtime. + * This enables using HybridObject callbacks (like addListener) from worklets + * and having shared value updates trigger useAnimatedStyle. + * + * Call this once at app startup. It will schedule the installation on the UI thread. + * + * @param runOnUI - The runOnUI function from react-native-reanimated + * + * @example + * ```tsx + * import { installWorkletDispatcher } from '@rive-app/react-native'; + * import { runOnUI } from 'react-native-reanimated'; + * + * // Call once at app startup + * installWorkletDispatcher(runOnUI); + * ``` + */ +export function installWorkletDispatcher( + runOnUI: ( + worklet: (...args: Args) => ReturnValue + ) => (...args: Args) => void +): void { + if (isInstalled) { + return; + } + isInstalled = true; + + // Create bridge on JS thread + const bridge = + NitroModules.createHybridObject('RiveWorkletBridge'); + + // Box it so we can use it in worklet + const boxedBridge = NitroModules.box(bridge); + + // Call install on Reanimated's UI runtime so dispatcher is installed there + runOnUI(() => { + 'worklet'; + const b = boxedBridge.unbox(); + b.install(); + })(); +} diff --git a/src/index.tsx b/src/index.tsx index f37bd9eb..323ae3d3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -56,3 +56,5 @@ export { useRiveFile } from './hooks/useRiveFile'; export { type RiveFileInput } from './hooks/useRiveFile'; export { type SetValueAction } from './types'; export { DataBindMode }; +export { installWorkletDispatcher } from './core/WorkletBridge'; +export type { RiveWorkletBridge } from './specs/RiveWorkletBridge.nitro'; diff --git a/src/specs/RiveWorkletBridge.nitro.ts b/src/specs/RiveWorkletBridge.nitro.ts new file mode 100644 index 00000000..fa26a331 --- /dev/null +++ b/src/specs/RiveWorkletBridge.nitro.ts @@ -0,0 +1,13 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +/** + * Bridge for installing Nitro Dispatcher on the worklets UI runtime. + */ +export interface RiveWorkletBridge + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + /** + * Install the dispatcher on the current runtime. + * Must be called from the UI runtime (via scheduleOnUI). + */ + install(): void; +} diff --git a/yarn.lock b/yarn.lock index 8592988a..87496116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/code-frame@npm:7.28.6" + dependencies: + "@babel/helper-validator-identifier": ^7.28.5 + js-tokens: ^4.0.0 + picocolors: ^1.1.1 + checksum: 6e98e47fd324b41c1919ff6d0fbf6fa5e991e5beff6b55803d9adaff9e11f4bc432803e52165f7b0d49af0f718209c3138a9b2fd51ff624b19d47704f11f8287 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.5": version: 7.28.5 resolution: "@babel/compat-data@npm:7.28.5" @@ -110,6 +121,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/generator@npm:7.28.6" + dependencies: + "@babel/parser": ^7.28.6 + "@babel/types": ^7.28.6 + "@jridgewell/gen-mapping": ^0.3.12 + "@jridgewell/trace-mapping": ^0.3.28 + jsesc: ^3.0.2 + checksum: 74f62f140e301c8c21652f7db3bc275008708272c0395f178ba6953297af50c4ea484874a44b3f292d242ce8a977fd3f31d9d3a3501c3aaca9cd46e3b1cded01 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -149,6 +173,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": ^7.27.3 + "@babel/helper-member-expression-to-functions": ^7.28.5 + "@babel/helper-optimise-call-expression": ^7.27.1 + "@babel/helper-replace-supers": ^7.28.6 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/traverse": ^7.28.6 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: f886ab302a83f8e410384aa635806b22374897fd9e3387c737ab9d91d1214bf9f7e57ae92619bd25dea63c9c0a49b25b44eb807873332e0eb9549219adc73639 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1": version: 7.28.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" @@ -233,6 +274,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: a0b4caab5e2180b215faa4d141ceac9e82fad9d446b8023eaeb8d82a6e62024726675b07fe8e616dd12f34e2bb59747e8d57aa8adab3e0717d1b8d691b118379 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" @@ -259,6 +307,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-replace-supers@npm:7.28.6" + dependencies: + "@babel/helper-member-expression-to-functions": ^7.28.5 + "@babel/helper-optimise-call-expression": ^7.27.1 + "@babel/traverse": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aa6530a52010883b6be88465e3b9e789509786a40203650a23a51c315f7442b196e5925fb8e2d66d1e3dc2c604cdc817bd8c5c170dbb322ab5ebc7486fd8a022 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" @@ -334,6 +395,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/parser@npm:7.28.6" + dependencies: + "@babel/types": ^7.28.6 + bin: + parser: ./bin/babel-parser.js + checksum: 2a35319792ceef9bc918f0ff854449bef0120707798fe147ef988b0606de226e2fbc3a562ba687148bfe5336c6c67358fb27e71a94e425b28482dcaf0b172fd6 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" @@ -668,6 +740,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-typescript@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -680,7 +763,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": +"@babel/plugin-transform-arrow-functions@npm:7.27.1, @babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" dependencies: @@ -739,7 +822,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": +"@babel/plugin-transform-class-properties@npm:7.27.1, @babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" dependencies: @@ -763,7 +846,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": +"@babel/plugin-transform-classes@npm:7.28.4, @babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-classes@npm:7.28.4" dependencies: @@ -1037,7 +1120,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" dependencies: @@ -1097,6 +1180,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c4428d31f182d724db6f10575669aad3dbccceb0dea26aa9071fa89f11b3456278da3097fcc78937639a13c105a82cd452dc0218ce51abdbcf7626a013b928a5 + languageName: node + linkType: hard + "@babel/plugin-transform-optional-chaining@npm:^7.0.0-0, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" @@ -1277,7 +1372,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.0.0-0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": +"@babel/plugin-transform-shorthand-properties@npm:7.27.1, @babel/plugin-transform-shorthand-properties@npm:^7.0.0-0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1" dependencies: @@ -1322,7 +1417,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.0.0-0, @babel/plugin-transform-template-literals@npm:^7.27.1": +"@babel/plugin-transform-template-literals@npm:7.27.1, @babel/plugin-transform-template-literals@npm:^7.0.0-0, @babel/plugin-transform-template-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-template-literals@npm:7.27.1" dependencies: @@ -1359,6 +1454,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-transform-typescript@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": ^7.27.3 + "@babel/helper-create-class-features-plugin": ^7.28.6 + "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/plugin-syntax-typescript": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 029add39a37e4a1960a43c3a109680462f631bc63cc8457ea65add2cce3271c9fd4d6a1782177c65ea5f77731e2f8e2bc65a9aec9cc826346ba540ecd0b97e5a + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.27.1" @@ -1382,7 +1492,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": +"@babel/plugin-transform-unicode-regex@npm:7.27.1, @babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1" dependencies: @@ -1515,6 +1625,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:7.27.1": + version: 7.27.1 + resolution: "@babel/preset-typescript@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-validator-option": ^7.27.1 + "@babel/plugin-syntax-jsx": ^7.27.1 + "@babel/plugin-transform-modules-commonjs": ^7.27.1 + "@babel/plugin-transform-typescript": ^7.27.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 38020f1b23e88ec4fbffd5737da455d8939244bddfb48a2516aef93fb5947bd9163fb807ce6eff3e43fa5ffe9113aa131305fef0fb5053998410bbfcfe6ce0ec + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7": version: 7.28.5 resolution: "@babel/preset-typescript@npm:7.28.5" @@ -1548,6 +1673,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/template@npm:7.28.6" + dependencies: + "@babel/code-frame": ^7.28.6 + "@babel/parser": ^7.28.6 + "@babel/types": ^7.28.6 + checksum: 8ab6383053e226025d9491a6e795293f2140482d14f60c1244bece6bf53610ed1e251d5e164de66adab765629881c7d9416e1e540c716541d2fd0f8f36a013d7 + languageName: node + linkType: hard + "@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5": version: 7.28.5 resolution: "@babel/traverse@npm:7.28.5" @@ -1563,6 +1699,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/traverse@npm:7.28.6" + dependencies: + "@babel/code-frame": ^7.28.6 + "@babel/generator": ^7.28.6 + "@babel/helper-globals": ^7.28.0 + "@babel/parser": ^7.28.6 + "@babel/template": ^7.28.6 + "@babel/types": ^7.28.6 + debug: ^4.3.1 + checksum: 07bc23b720d111a20382fcdba776b800a7c1f94e35f8e4f417869f6769ba67c2b9573c8240924ca3b0ee5a88fa7ed048efb289e8b324f5cb4971e771174a0d32 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.28.5 resolution: "@babel/types@npm:7.28.5" @@ -1573,6 +1724,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/types@npm:7.28.6" + dependencies: + "@babel/helper-string-parser": ^7.27.1 + "@babel/helper-validator-identifier": ^7.28.5 + checksum: f76556cda59be337cc10dc68b2a9a947c10de018998bab41076e7b7e4489b28dd53299f98f22eec0774264c989515e6fdc56de91c73e3aa396367bb953200a6a + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -7105,7 +7266,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^2.0.0": +"convert-source-map@npm:2.0.0, convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 @@ -8634,11 +8795,11 @@ __metadata: react-native: 0.81.5 react-native-gesture-handler: 2.29.1 react-native-nitro-modules: 0.33.2 - react-native-reanimated: 4.1.5 + react-native-reanimated: 4.2.1 react-native-safe-area-context: ~5.6.0 react-native-screens: ~4.16.0 react-native-web: ~0.21.0 - react-native-worklets: 0.6.1 + react-native-worklets: 0.7.1 typescript: ~5.9.2 languageName: unknown linkType: soft @@ -14398,7 +14559,7 @@ __metadata: languageName: node linkType: hard -"react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": +"react-native-is-edge-to-edge@npm:1.2.1, react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": version: 1.2.1 resolution: "react-native-is-edge-to-edge@npm:1.2.1" peerDependencies: @@ -14443,6 +14604,20 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@npm:4.2.1": + version: 4.2.1 + resolution: "react-native-reanimated@npm:4.2.1" + dependencies: + react-native-is-edge-to-edge: 1.2.1 + semver: 7.7.3 + peerDependencies: + react: "*" + react-native: "*" + react-native-worklets: ">=0.7.0" + checksum: 102ead7c02411227f8364dec58cffc7a9225c8b9733cce07054064ecfd90cf2b124c8d1caf2bd11fc7db8603e0c91c392957ab0ce5852dcd1b3669e7d7d59931 + languageName: node + linkType: hard + "react-native-rive-example@workspace:example": version: 0.0.0-use.local resolution: "react-native-rive-example@workspace:example" @@ -14544,6 +14719,29 @@ __metadata: languageName: node linkType: hard +"react-native-worklets@npm:0.7.1": + version: 0.7.1 + resolution: "react-native-worklets@npm:0.7.1" + dependencies: + "@babel/plugin-transform-arrow-functions": 7.27.1 + "@babel/plugin-transform-class-properties": 7.27.1 + "@babel/plugin-transform-classes": 7.28.4 + "@babel/plugin-transform-nullish-coalescing-operator": 7.27.1 + "@babel/plugin-transform-optional-chaining": 7.27.1 + "@babel/plugin-transform-shorthand-properties": 7.27.1 + "@babel/plugin-transform-template-literals": 7.27.1 + "@babel/plugin-transform-unicode-regex": 7.27.1 + "@babel/preset-typescript": 7.27.1 + convert-source-map: 2.0.0 + semver: 7.7.3 + peerDependencies: + "@babel/core": "*" + react: "*" + react-native: "*" + checksum: d6ca920ce53cad6ad45ac8379914adfaf73a92d76dc7c68d9b8a8a2913f7042ec8d60bbb6fcf72afe593993d087cbe6277c3f81927d2c0ade9a94acaf58b5dc3 + languageName: node + linkType: hard + "react-native@npm:0.79.2": version: 0.79.2 resolution: "react-native@npm:0.79.2" @@ -15297,21 +15495,21 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": - version: 6.3.1 - resolution: "semver@npm:6.3.1" +"semver@npm:7.7.3, semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1": + version: 7.7.3 + resolution: "semver@npm:7.7.3" bin: semver: bin/semver.js - checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 languageName: node linkType: hard -"semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1": - version: 7.7.3 - resolution: "semver@npm:7.7.3" +"semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" bin: semver: bin/semver.js - checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 languageName: node linkType: hard