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 (
-