From 681930ea2fe714d2237a118c432f050394ff9b3a Mon Sep 17 00:00:00 2001 From: Charles P Date: Wed, 3 Apr 2024 11:20:04 -0700 Subject: [PATCH] feat: example app draws rectangles (#28) --- .eslintrc.js | 6 +- .github/workflows/ci.yml | 11 +- .../objectdetection/ObjectDetectorHelper.kt | 14 +- example/package.json | 4 +- example/src/App.tsx | 1 - examples/objectdetection/README.md | 1 + examples/objectdetection/ios/Podfile.lock | 10 + examples/objectdetection/package.json | 1 + examples/objectdetection/src/App.tsx | 176 ++++++++++++++++-- .../ObjectDetectionFrameProcessorPlugin.swift | 10 +- .../ObjectDetectorHelper.swift | 24 ++- package.json | 1 - src/index.tsx | 29 ++- src/objectDetection/index.ts | 69 +++---- tsconfig.json | 1 + yarn.lock | 59 ++++++ 16 files changed, 324 insertions(+), 93 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d958d96..93fb984 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,7 +5,9 @@ module.exports = { tsconfigRootDir: __dirname, project: [ "./tsconfig.json", - "docsite/tsconfig.json" + "docsite/tsconfig.json", + "examples/objectdetection/tsconfig.json", + "example/tsconfig.json", ], ecmaFeatures: { jsx: true, @@ -22,6 +24,8 @@ module.exports = { ".prettierrc.js", "*.config.js", "jest.setup.js", + "coverage", + "example/index.js", ], plugins: ["@typescript-eslint"], extends: [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2eb0cb5..15990ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,12 +130,6 @@ jobs: echo "turbo_cache_hit=1" >> $GITHUB_ENV fi - - name: Generate composite hash - id: composite-hash - run: | - cat example/ios/Podfile.lock examples/objectdetection/ios/Podfile.lock > combined.lock - echo "COMPOSITE_HASH=$(sha256sum combined.lock | awk '{print $1}')" >> $GITHUB_ENV - - name: Cache cocoapods if: env.turbo_cache_hit != 1 id: cocoapods-cache @@ -143,16 +137,13 @@ jobs: with: path: | **/ios/Pods - key: ${{ runner.os }}-cocoapods-${{ env.COMPOSITE_HASH }} + key: ${{ runner.os }}-cocoapods-${{ hashFiles('examples/objectdetection/ios/Podfile.lock') }} restore-keys: | ${{ runner.os }}-cocoapods- - name: Install cocoapods if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' run: | - cd example/ios - pod install - cd ../.. cd examples/objectdetection/ios pod install env: diff --git a/android/src/main/java/com/reactnativemediapipe/objectdetection/ObjectDetectorHelper.kt b/android/src/main/java/com/reactnativemediapipe/objectdetection/ObjectDetectorHelper.kt index d3f710d..676c1d1 100644 --- a/android/src/main/java/com/reactnativemediapipe/objectdetection/ObjectDetectorHelper.kt +++ b/android/src/main/java/com/reactnativemediapipe/objectdetection/ObjectDetectorHelper.kt @@ -13,6 +13,8 @@ import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicYuvToRGB import android.renderscript.Type import android.util.Log +import android.os.Handler +import android.os.Looper import androidx.core.math.MathUtils.clamp import com.facebook.react.common.annotations.VisibleForTesting import com.google.mediapipe.framework.image.BitmapImageBuilder @@ -48,8 +50,14 @@ class ObjectDetectorHelper( fun clearObjectDetector() { objectDetectorListener = null - objectDetector?.close() - objectDetector = null + // This is a hack. If we call close directly, we crash. There is a theory + // that this is because the object detector is still doing some processing, and that + // it is not safe to close it. So a better solution might be to mark it and then when + // processing is complete, cause it to be closed. + Handler(Looper.getMainLooper()).postDelayed({ + objectDetector?.close() + objectDetector = null + }, 100) } // Initialize the object detector using current settings on the @@ -405,7 +413,7 @@ class ObjectDetectorHelper( // Convert the input Bitmap object to an MPImage object to run inference // val bitmap = toBitmap(image) // val bitmap = yuv420ToBitmap(image) - val bitmap = yuv420ToBitmapRS(image,context) + val bitmap = yuv420ToBitmapRS(image, context) // val mpImage = MediaImageBuilder(image).build() val mpImage = BitmapImageBuilder(bitmap).build() diff --git a/example/package.json b/example/package.json index f115412..4f82470 100644 --- a/example/package.json +++ b/example/package.json @@ -8,7 +8,7 @@ "ios": "react-native run-ios", "start": "react-native start", "build:android-disabled": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", - "build:ios": "cd ios && xcodebuild -workspace MediapipeExample.xcworkspace -scheme MediapipeExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" + "build:ios-disabled": "cd ios && xcodebuild -workspace MediapipeExample.xcworkspace -scheme MediapipeExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { "react": "18.2.0", @@ -28,4 +28,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/example/src/App.tsx b/example/src/App.tsx index f5a9db6..d72aaa8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -60,7 +60,6 @@ const styles = StyleSheet.create({ flex: 1, alignSelf: "stretch", }, - permsButton: {}, noPermsText: { fontSize: 20, fontWeight: "bold", diff --git a/examples/objectdetection/README.md b/examples/objectdetection/README.md index 12470c3..178b0c2 100644 --- a/examples/objectdetection/README.md +++ b/examples/objectdetection/README.md @@ -1,3 +1,4 @@ +# React Native Mediapipe This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). # Getting Started diff --git a/examples/objectdetection/ios/Podfile.lock b/examples/objectdetection/ios/Podfile.lock index ba05386..b83e73c 100644 --- a/examples/objectdetection/ios/Podfile.lock +++ b/examples/objectdetection/ios/Podfile.lock @@ -947,6 +947,12 @@ PODS: - React-Mapbuffer (0.73.6): - glog - React-debug + - react-native-skia (1.0.5): + - glog + - RCT-Folly (= 2022.05.16.00) + - React + - React-callinvoker + - React-Core - react-native-worklets-core (0.4.0): - React - React-callinvoker @@ -1185,6 +1191,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - "react-native-skia (from `../node_modules/@shopify/react-native-skia`)" - react-native-worklets-core (from `../node_modules/react-native-worklets-core`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) @@ -1284,6 +1291,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: :path: "../node_modules/react-native/ReactCommon" + react-native-skia: + :path: "../node_modules/@shopify/react-native-skia" react-native-worklets-core: :path: "../node_modules/react-native-worklets-core" React-nativeconfig: @@ -1375,6 +1384,7 @@ SPEC CHECKSUMS: React-jsinspector: 85583ef014ce53d731a98c66a0e24496f7a83066 React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab + react-native-skia: 6da30aeaa315fcec5c27ebfb016b9eca2b57b66f react-native-worklets-core: 2efe80a3ee87fe5e6fefa814e0e20c2708d3ad25 React-nativeconfig: b4d4e9901d4cabb57be63053fd2aa6086eb3c85f React-NativeModulesApple: cd26e56d56350e123da0c1e3e4c76cb58a05e1ee diff --git a/examples/objectdetection/package.json b/examples/objectdetection/package.json index 7955bbd..a2b379b 100644 --- a/examples/objectdetection/package.json +++ b/examples/objectdetection/package.json @@ -13,6 +13,7 @@ "test": "jest" }, "dependencies": { + "@shopify/react-native-skia": "^1.0.5", "react": "18.2.0", "react-native": "0.73.6", "react-native-vision-camera": "^3.9.2", diff --git a/examples/objectdetection/src/App.tsx b/examples/objectdetection/src/App.tsx index cae32f0..1698608 100644 --- a/examples/objectdetection/src/App.tsx +++ b/examples/objectdetection/src/App.tsx @@ -1,13 +1,41 @@ +import { + Canvas, + Group, + Rect, + Text as SkiaText, + matchFont, +} from "@shopify/react-native-skia"; import * as React from "react"; -import { Pressable, StyleSheet, Text, View } from "react-native"; -import { MediapipeCamera } from "react-native-mediapipe"; +import { + Platform, + Pressable, + StyleSheet, + Text, + View, + useWindowDimensions, +} from "react-native"; +import { + Delegate, + MediapipeCamera, + RunningMode, + useObjectDetection, +} from "react-native-mediapipe"; import { useCameraPermission, useMicrophonePermission, } from "react-native-vision-camera"; +interface Detection { + label: string; + x: number; + y: number; + width: number; + height: number; +} + export default function App(): React.ReactElement | null { + const { width, height } = useWindowDimensions(); const camPerm = useCameraPermission(); const micPerm = useMicrophonePermission(); const [permsGranted, setPermsGranted] = React.useState<{ @@ -31,39 +59,149 @@ export default function App(): React.ReactElement | null { }); } }, [camPerm, micPerm]); - console.log("App", permsGranted); + + const [objectFrames, setObjectFrames] = React.useState([]); + + const frameProcessor = useObjectDetection( + (results) => { + console.log(results); + const firstResult = results.results[0]; + const detections = firstResult?.detections ?? []; + setObjectFrames( + detections.map((detection) => { + return { + label: detection.categories[0]?.categoryName ?? "unknown", + x: (detection.boundingBox.left / results.inputImageWidth) * width, + y: (detection.boundingBox.top / results.inputImageHeight) * height, + width: + ((detection.boundingBox.right - detection.boundingBox.left) / + results.inputImageWidth) * + width, + height: + ((detection.boundingBox.bottom - detection.boundingBox.top) / + results.inputImageHeight) * + height, + }; + }) + ); + }, + (error) => { + console.error(`onError: ${error}`); + }, + RunningMode.LIVE_STREAM, + "efficientdet-lite0.tflite", + { delegate: Delegate.GPU } + ); + if (permsGranted.cam && permsGranted.mic) { + return ( + + + + {objectFrames.map((frame, index) => ( + + ))} + + + ); + } else { + return ; + } +} + +const NeedPermissions: React.FC<{ askForPermissions: () => void }> = ({ + askForPermissions, +}) => { return ( - {permsGranted.cam && permsGranted.mic ? ( - - ) : ( - <> - - Camera and Mic permissions required - - - Request - - - )} + + Camera and Mic permissions required + + + Request + ); -} +}; + +const ObjectFrame: React.FC<{ frame: Detection; index: number }> = ({ + frame, + index, +}) => { + const color = colorNames[index % colorNames.length]; + return ( + + + + + ); +}; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", justifyContent: "center", + position: "relative", }, box: { - flex: 1, - alignSelf: "stretch", + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + }, + permsButton: { + padding: 10, + backgroundColor: "lightblue", + borderRadius: 5, + margin: 10, }, - permsButton: {}, noPermsText: { fontSize: 20, fontWeight: "bold", color: "red", }, }); + +const colorNames = [ + "Coral", + "DarkCyan", + "DeepSkyBlue", + "ForestGreen", + "GoldenRod", + "MediumOrchid", + "SteelBlue", + "Tomato", + "Turquoise", + "SlateGray", + "DodgerBlue", + "FireBrick", + "Gold", + "HotPink", + "LimeGreen", + "Navy", + "OrangeRed", + "RoyalBlue", + "SeaGreen", + "Violet", +]; + +const fontFamily = Platform.select({ ios: "Helvetica", android: "sans-serif" }); +const fontStyle = { + fontFamily, + fontSize: 14, +}; +const font = matchFont(fontStyle); diff --git a/ios/objectdetection/ObjectDetectionFrameProcessorPlugin.swift b/ios/objectdetection/ObjectDetectionFrameProcessorPlugin.swift index 1f74fa4..c972414 100644 --- a/ios/objectdetection/ObjectDetectionFrameProcessorPlugin.swift +++ b/ios/objectdetection/ObjectDetectionFrameProcessorPlugin.swift @@ -17,11 +17,15 @@ public class ObjectDetectionFrameProcessorPlugin: FrameProcessorPlugin { public override func callback(_ frame: Frame, withArguments arguments: [AnyHashable: Any]?) -> Any { - let detectorHandle: Double = arguments?["detectorHandle"] as! Double - guard let detector = ObjectDetectionModule.detectorMap[Int(detectorHandle)] else { + guard let detectorHandleValue = arguments?["detectorHandle"] as? Double else { return false } - + + // Now that we have a valid Double, attempt to retrieve the detector using it + guard let detector = ObjectDetectionModule.detectorMap[Int(detectorHandleValue)] else { + return false + } + let buffer = frame.buffer detector.detectAsync( sampleBuffer: buffer, diff --git a/ios/objectdetection/ObjectDetectorHelper.swift b/ios/objectdetection/ObjectDetectorHelper.swift index 068886d..00d80d3 100644 --- a/ios/objectdetection/ObjectDetectorHelper.swift +++ b/ios/objectdetection/ObjectDetectorHelper.swift @@ -24,6 +24,12 @@ class ObjectDetectorHelper: NSObject { var modelPath: String let handle: Int + // this is an unfortunate hack : we need to provide the client with the size of the + // image which was being analyzed. This information is helpfully provided except in the + // case of livestream. So we stash it here for each frame. It changes seldom so + // this should rarely be an issue + private var livestreamImageSize: CGSize = CGSize(width: 0, height: 0) + // MARK: - Custom Initializer init( handle:Int, @@ -74,12 +80,15 @@ class ObjectDetectorHelper: NSObject { guard let mpImage = try? MPImage(uiImage: image) else { return nil } - print(image.imageOrientation.rawValue) do { let startDate = Date() let result = try objectDetector?.detect(image: mpImage) let inferenceTime = Date().timeIntervalSince(startDate) * 1000 - return ResultBundle(inferenceTime: inferenceTime, objectDetectorResults: [result]) + return ResultBundle( + inferenceTime: inferenceTime, + objectDetectorResults: [result], + size: CGSizeMake(CGFloat(image.size.width), CGFloat(image.size.height)) + ) } catch { print(error) return nil @@ -94,6 +103,7 @@ class ObjectDetectorHelper: NSObject { return } do { + self.livestreamImageSize = CGSize(width: image.width, height: image.height) try objectDetector?.detectAsync(image: image, timestampInMilliseconds: timeStamps) } catch { print(error) @@ -162,7 +172,9 @@ class ObjectDetectorHelper: NSObject { prevTime = curTime let resultBundle = ResultBundle( inferenceTime: inferenceTime, - objectDetectorResults: [result]) + objectDetectorResults: [result], + size: CGSizeMake(CGFloat(image.width), CGFloat(image.height)) + ) delegate?.objectDetectorHelper(self, onResults: resultBundle, error: nil) } catch { delegate?.objectDetectorHelper(self, onResults: nil, error: error) @@ -186,7 +198,9 @@ extension ObjectDetectorHelper: ObjectDetectorLiveStreamDelegate { } let resultBundle = ResultBundle( inferenceTime: Date().timeIntervalSince1970 * 1000 - Double(timestampInMilliseconds), - objectDetectorResults: [result]) + objectDetectorResults: [result], + size: CGSize(width: livestreamImageSize.width, height: livestreamImageSize.height) + ) delegate?.objectDetectorHelper(self, onResults: resultBundle, error: nil) } } @@ -195,7 +209,7 @@ extension ObjectDetectorHelper: ObjectDetectorLiveStreamDelegate { struct ResultBundle { let inferenceTime: Double let objectDetectorResults: [ObjectDetectorResult?] - var size: CGSize = .zero + let size: CGSize } struct DefaultConstants { diff --git a/package.json b/package.json index acd6c77..c796461 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "!**/.*" ], "scripts": { - "example": "yarn workspace react-native-mediapipe-example", "test": "jest", "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", diff --git a/src/index.tsx b/src/index.tsx index 7f0c172..d48a7f3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,33 +1,32 @@ import React from "react"; import { type ViewStyle, Text, Platform } from "react-native"; -import { Camera, useCameraDevice } from "react-native-vision-camera"; -import { RunningMode, useObjectDetection } from "./objectDetection"; +import { + Camera, + useCameraDevice, + type FrameProcessor, +} from "react-native-vision-camera"; -type MediapipeProps = { +export type MediapipeCameraProps = { style: ViewStyle; + processor: FrameProcessor; }; -export const MediapipeCamera: React.FC = ({ style }) => { +export const MediapipeCamera: React.FC = ({ + style, + processor, +}) => { const device = useCameraDevice("front"); - const frameProcessor = useObjectDetection( - (results) => { - console.log(results); - }, - (error) => { - console.log(error); - }, - RunningMode.LIVE_STREAM, - "efficientdet-lite0.tflite" - ); return device !== undefined ? ( ) : ( no device ); }; + +export * from "./objectDetection"; diff --git a/src/objectDetection/index.ts b/src/objectDetection/index.ts index 0b2a3d5..60799b1 100644 --- a/src/objectDetection/index.ts +++ b/src/objectDetection/index.ts @@ -2,14 +2,17 @@ import React from "react"; import { NativeEventEmitter, NativeModules } from "react-native"; import { VisionCameraProxy, - type FrameProcessorPlugin, useFrameProcessor, } from "react-native-vision-camera"; -import { useSharedValue } from "react-native-worklets-core"; const { ObjectDetection } = NativeModules; const eventEmitter = new NativeEventEmitter(ObjectDetection); +const plugin = VisionCameraProxy.initFrameProcessorPlugin("objectDetection"); +if (!plugin) { + throw new Error("Failed to initialize objectdetection plugin"); +} + interface ObjectDetectionModule { createDetector: ( threshold: number, @@ -48,10 +51,10 @@ interface DetectionMap { } interface RectFMap { - xmin: number; - ymin: number; - xmax: number; - ymax: number; + left: number; + top: number; + right: number; + bottom: number; } interface CategoryMap { @@ -74,7 +77,7 @@ interface ObjectDetectionError { } // eslint-disable-next-line no-restricted-syntax -enum Delegate { +export enum Delegate { CPU = 0, GPU = 1, } @@ -101,7 +104,6 @@ const detectorMap: Map = new Map(); eventEmitter.addListener( "onResults", (args: { handle: number } & ResultBundleMap) => { - console.log("onResults", JSON.stringify(args)); const callbacks = detectorMap.get(args.handle); if (callbacks) { callbacks.onResults(args); @@ -111,7 +113,6 @@ eventEmitter.addListener( eventEmitter.addListener( "onError", (args: { handle: number } & ObjectDetectionError) => { - console.log("onError", JSON.stringify(args)); const callbacks = detectorMap.get(args.handle); if (callbacks) { callbacks.onError(args); @@ -126,41 +127,45 @@ export function useObjectDetection( model: string, options?: Partial ) { - console.log("useObjectDetection", { runningMode, model, options }); - const processor = useSharedValue< - | { detectorHandle: number; plugin: FrameProcessorPlugin | undefined } - | undefined - >(undefined); + const [detectorHandle, setDetectorHandle] = React.useState< + number | undefined + >(); // Remember the latest callback if it changes. React.useLayoutEffect(() => { - if (processor.value?.detectorHandle !== undefined) { - detectorMap.set(processor.value.detectorHandle, { onResults, onError }); + if (detectorHandle !== undefined) { + detectorMap.set(detectorHandle, { onResults, onError }); } - }, [onResults, onError, processor.value?.detectorHandle]); + }, [onResults, onError, detectorHandle]); React.useEffect(() => { - const plugin = - VisionCameraProxy.initFrameProcessorPlugin("objectDetection"); - + let newHandle: number | undefined; getObjectDetectionModule() .createDetector( options?.threshold ?? 0.5, options?.maxResults ?? 3, - options?.delegate ?? Delegate.GPU, + options?.delegate ?? Delegate.CPU, model, runningMode ) .then((handle) => { - console.log("useObjectDetection", runningMode, model, handle); - processor.value = { detectorHandle: handle, plugin }; + console.log( + "useObjectDetection.createDetector", + runningMode, + model, + handle + ); + setDetectorHandle(handle); + newHandle = handle; }); return () => { - console.log("useObjectDetection.useEffect.unsub", "releaseDetector"); - if (processor.value?.detectorHandle !== undefined) { - getObjectDetectionModule().releaseDetector( - processor.value.detectorHandle - ); + console.log( + "useObjectDetection.useEffect.unsub", + "releaseDetector", + newHandle + ); + if (newHandle !== undefined) { + getObjectDetectionModule().releaseDetector(newHandle); } }; }, [ @@ -168,18 +173,16 @@ export function useObjectDetection( options?.maxResults, runningMode, options?.threshold, - processor, model, ]); - console.log("useObjectDetection", { processor: processor.value }); const frameProcessor = useFrameProcessor( (frame) => { "worklet"; - processor.value?.plugin?.call(frame, { - detectorHandle: processor.value.detectorHandle, + plugin?.call(frame, { + detectorHandle, }); }, - [processor.value?.detectorHandle, processor.value?.plugin] + [detectorHandle] ); return frameProcessor; } diff --git a/tsconfig.json b/tsconfig.json index 95e5967..a3f369d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,6 @@ "babel.config.js", "react-native.config.js", "docsite", + "example", ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 476ee11..96e2c72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5131,6 +5131,27 @@ __metadata: languageName: node linkType: hard +"@shopify/react-native-skia@npm:^1.0.5": + version: 1.0.5 + resolution: "@shopify/react-native-skia@npm:1.0.5" + dependencies: + canvaskit-wasm: 0.39.1 + react-reconciler: 0.27.0 + peerDependencies: + react: ">=18.0" + react-native: ">=0.64" + react-native-reanimated: ">=2.0.0" + peerDependenciesMeta: + react-native: + optional: true + react-native-reanimated: + optional: true + bin: + setup-skia-web: scripts/setup-canvaskit.js + checksum: 1879b1cbfbc6176f84fcab8db15d1f8ceea8432fcbfdea83832a1c906195ba463456e64a31d6389d4f5b9c727f94c0f4d8475c615e51d3444cb138860ad2a09d + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.5": version: 4.1.5 resolution: "@sideway/address@npm:4.1.5" @@ -6238,6 +6259,13 @@ __metadata: languageName: node linkType: hard +"@webgpu/types@npm:0.1.21": + version: 0.1.21 + resolution: "@webgpu/types@npm:0.1.21" + checksum: b16ddc4d46a3c507a7f243f14b08b96ebbdb4495f9ea703441ad3525dd5e7e4a5f743f2879e51d74c78846f950e7d60c1349670f75cb7ca03ac3ddf51d46aabe + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -7453,6 +7481,15 @@ __metadata: languageName: node linkType: hard +"canvaskit-wasm@npm:0.39.1": + version: 0.39.1 + resolution: "canvaskit-wasm@npm:0.39.1" + dependencies: + "@webgpu/types": 0.1.21 + checksum: da62926fc81f424a781e148b4d76bb5fc9b0188f136090b3b287522dc653cb002bfb406e2eff45b55fcc1cafbc7629f988e20ad6c777bab85c1bb09e1091a5e2 + languageName: node + linkType: hard + "ccount@npm:^2.0.0": version: 2.0.1 resolution: "ccount@npm:2.0.1" @@ -15722,6 +15759,7 @@ __metadata: "@react-native/eslint-config": 0.73.2 "@react-native/metro-config": 0.73.5 "@react-native/typescript-config": 0.73.1 + "@shopify/react-native-skia": ^1.0.5 "@types/react": ^18.2.6 "@types/react-test-renderer": ^18.0.0 babel-jest: ^29.6.3 @@ -17588,6 +17626,18 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:0.27.0": + version: 0.27.0 + resolution: "react-reconciler@npm:0.27.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.21.0 + peerDependencies: + react: ^18.0.0 + checksum: c2ae111f150c2a46970182df12ea8254719fdfec5e26574711b1838fc37863c63671460a351570fd359c088d891e7bb0ff89023c2f7c1582393b57dd517b92c2 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.0 resolution: "react-refresh@npm:0.14.0" @@ -18458,6 +18508,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.21.0": + version: 0.21.0 + resolution: "scheduler@npm:0.21.0" + dependencies: + loose-envify: ^1.1.0 + checksum: 4f8285076041ed2c81acdd1faa987f1655fdbd30554bc667c1ea64743fc74fb3a04ca7d27454b3d678735df5a230137a3b84756061b43dc5796e80701b66d124 + languageName: node + linkType: hard + "scheduler@npm:^0.23.0": version: 0.23.0 resolution: "scheduler@npm:0.23.0"