diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..bf357fbbc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "all" +} diff --git a/apps/example/getWebMetroConfig.js b/apps/example/getWebMetroConfig.js new file mode 100644 index 000000000..93b7475d9 --- /dev/null +++ b/apps/example/getWebMetroConfig.js @@ -0,0 +1,34 @@ +const path = require("path"); +const root = path.resolve(__dirname, "../.."); +const r3fPath = path.resolve(root, "node_modules/@react-three/fiber"); +const rnwPath = path.resolve(root, "node_modules/react-native-web"); +const assetRegistryPath = path.resolve( + root, + "node_modules/react-native-web/dist/modules/AssetRegistry/index", +); + +module.exports = function(metroConfig){ + metroConfig.resolver.platforms = ["ios", "android", "web"]; + const origResolveRequest = metroConfig.resolver.resolveRequest; + metroConfig.resolver.resolveRequest = (contextRaw, moduleName, platform) => { + const context = { + ...contextRaw, + preferNativePlatform: false, + }; + + if (moduleName === "react-native") { + return { + filePath: path.resolve(rnwPath, "dist/index.js"), + type: "sourceFile", + }; + } + + // Let default config handle other modules + return origResolveRequest(context, moduleName, platform); + }; + + metroConfig.transformer.assetRegistryPath = assetRegistryPath; + + return metroConfig +} + diff --git a/apps/example/index.html b/apps/example/index.html new file mode 100644 index 000000000..5c1245cc8 --- /dev/null +++ b/apps/example/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + Example + + + + +
+ + + diff --git a/apps/example/index.web.js b/apps/example/index.web.js new file mode 100644 index 000000000..5c70701f8 --- /dev/null +++ b/apps/example/index.web.js @@ -0,0 +1,21 @@ +import { AppRegistry } from "react-native"; +import App from "./src/App"; +import { name as appName } from "./app.json"; + +AppRegistry.registerComponent(appName, () => App); + +const rootTag = document.getElementById("root"); +if (process.env.NODE_ENV !== "production") { + if (!rootTag) { + throw new Error( + 'Required HTML element with id "root" was not found in the document HTML.', + ); + } +} + +CanvasKitInit({ + locateFile: (file) => `https://unpkg.com/canvaskit-wasm/bin/full/${file}`, +}).then((CanvasKit) => { + window.CanvasKit = global.CanvasKit = CanvasKit; + AppRegistry.runApplication(appName, { rootTag }); +}); diff --git a/apps/example/metro.config.js b/apps/example/metro.config.js index 42ae30faf..f9fc66312 100644 --- a/apps/example/metro.config.js +++ b/apps/example/metro.config.js @@ -1,5 +1,6 @@ const { makeMetroConfig } = require("@rnx-kit/metro-config"); const path = require('path'); +const getWebMetroConfig = require('./getWebMetroConfig'); const root = path.resolve(__dirname, '../..'); const threePackagePath = path.resolve(root, 'node_modules/three'); @@ -17,18 +18,26 @@ const extraConfig = { type: 'sourceFile', }; } - if (moduleName === 'three' || moduleName === 'three/webgpu') { + if (moduleName === 'three' || moduleName === 'three/webgpu') { return { filePath: path.resolve(threePackagePath, 'build/three.webgpu.js'), type: 'sourceFile', }; } - if (moduleName === 'three/tsl') { + if (moduleName === 'three/tsl') { return { filePath: path.resolve(threePackagePath, 'build/three.tsl.js'), type: 'sourceFile', }; } + + if (moduleName === "@react-three/fiber") { + //Just use the vanilla web build of react three fiber, not the stale "native" code path which has not been kept up to date. + return { + filePath: path.resolve(r3fPath, "dist/react-three-fiber.esm.js"), + type: "sourceFile", + }; + } // Let Metro handle other modules return context.resolveRequest(context, moduleName, platform); }, @@ -47,5 +56,4 @@ const extraConfig = { const metroConfig = makeMetroConfig(extraConfig); metroConfig.resolver.assetExts.push('glb', 'gltf', 'jpg', 'bin', 'hdr'); - -module.exports = metroConfig; +module.exports = !!process.env.IS_WEB_BUILD ? getWebMetroConfig(metroConfig) : metroConfig; \ No newline at end of file diff --git a/apps/example/package.json b/apps/example/package.json index 7dce8612e..ac03c9a64 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -7,6 +7,7 @@ "tsc": "tsc --noEmit", "android": "react-native run-android", "ios": "react-native run-ios", + "web": "IS_WEB_BUILD=true react-native start", "start": "react-native start", "pod:install:ios": "pod install --project-directory=ios", "pod:install:macos": "pod install --project-directory=macos", @@ -19,18 +20,21 @@ "@callstack/react-native-visionos": "^0.74.0", "@react-navigation/native": "^6.1.17", "@react-navigation/stack": "^6.4.0", - "@react-three/fiber": "^8.17.6", + "@react-three/fiber": "^9.1.2", "@shopify/react-native-skia": "2.0.0", "@tensorflow/tfjs": "^4.22.0", "@tensorflow/tfjs-backend-webgpu": "^4.22.0", "@tensorflow/tfjs-vis": "^1.5.1", + "@types/react-dom": "^19.1.5", "fast-text-encoding": "^1.0.6", "react": "19.0.0", + "react-dom": "19.0.0", "react-native": "0.79.2", "react-native-gesture-handler": "^2.17.1", "react-native-macos": "^0.78.3", "react-native-reanimated": "^3.12.1", - "react-native-safe-area-context": "^5.4.0", + "react-native-safe-area-context": "^5.4.1", + "react-native-web": "^0.20.0", "react-native-wgpu": "*", "teapot": "^1.0.0", "three": "0.172.0", diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index 9e5a6facc..3b4ceb6b7 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -44,7 +44,10 @@ function App() { return ( - + (null); -const fontFamily = Platform.select({ ios: "Helvetica", default: "serif" }); -const fontStyle = { - fontFamily, - fontSize: 200, -}; -const font = matchFont(fontStyle); + const font = useFont(require("../assets/helvetica.ttf")); -const paint = Skia.Paint(); -paint.setColor(Skia.Color("black")); -paint.setStyle(PaintStyle.Stroke); -paint.setStrokeWidth(1); + // Lazy initialize skia derived constants + if (!skiaConstants.current) { + const { width } = Dimensions.get("window"); -const grid = Skia.Path.Make(); -const cellSize = width / SIZE; + const paint = Skia.Paint(); + paint.setColor(Skia.Color("black")); + paint.setStyle(PaintStyle.Stroke); + paint.setStrokeWidth(1); -grid.moveTo(0, 0); + const grid = Skia.Path.Make(); + const cellSize = width / SIZE; -// Draw vertical lines -for (let i = 0; i <= SIZE; i++) { - grid.moveTo(i * cellSize, 0); - grid.lineTo(i * cellSize, width); -} + grid.moveTo(0, 0); -// Draw horizontal lines -for (let i = 0; i <= SIZE; i++) { - grid.moveTo(0, i * cellSize); - grid.lineTo(width, i * cellSize); -} + // Draw vertical lines + for (let i = 0; i <= SIZE; i++) { + grid.moveTo(i * cellSize, 0); + grid.lineTo(i * cellSize, width); + } -const f = 1 / cellSize; + // Draw horizontal lines + for (let i = 0; i <= SIZE; i++) { + grid.moveTo(0, i * cellSize); + grid.lineTo(width, i * cellSize); + } + + const f = 1 / cellSize; + + skiaConstants.current = { + f, + paint, + grid, + width, + }; + } + + const { f, paint, grid, width } = skiaConstants.current; -export function MNISTInference() { const { device } = useDevice(); const network = useRef(); const text = useSharedValue(""); @@ -84,21 +103,26 @@ export function MNISTInference() { if (surface.value) { const canvas = surface.value.getCanvas(); canvas.drawPath(path.value, paint); + surface.value.flush(); image.value = surface.value!.makeImageSnapshot(); const pixels = image.value.readPixels(0, 0, { width: SIZE, height: SIZE, - alphaType: AlphaType.Opaque, - colorType: ColorType.Alpha_8, + colorType: ColorType.RGBA_8888, + alphaType: AlphaType.Unpremul, }); + + const gray = new Uint8Array(SIZE * SIZE); + for (let i = 0; i < SIZE * SIZE; i++) { + gray[i] = pixels![i * 4]; + } + runOnJS(runInference)( - centerData(pixels as Uint8Array).map( - (x) => (x / 255) * 3.24 - 0.42, - ), + centerData(gray).map((x) => (x / 255) * 3.24 - 0.42), ); } }); - }, [path, runInference, surface, image]); + }, [path, runInference, surface, image, f, paint]); useEffect(() => { (async () => { @@ -111,6 +135,11 @@ export function MNISTInference() { })(); })(); }, [device, network, surface]); + + if (!font) { + return null; + } + return (