Skip to content

Commit 5c58cae

Browse files
authored
feat(🔺): experimental WebGPU Canvas integration (#2632)
1 parent a7970cf commit 5c58cae

25 files changed

+509
-1
lines changed

apps/paper/ios/Podfile.lock

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,27 @@ PODS:
12831283
- ReactCommon/turbomodule/bridging
12841284
- ReactCommon/turbomodule/core
12851285
- Yoga
1286+
- react-native-wgpu (0.1.7):
1287+
- DoubleConversion
1288+
- glog
1289+
- hermes-engine
1290+
- RCT-Folly (= 2024.01.01.00)
1291+
- RCTRequired
1292+
- RCTTypeSafety
1293+
- React-Core
1294+
- React-debug
1295+
- React-Fabric
1296+
- React-featureflags
1297+
- React-graphics
1298+
- React-ImageManager
1299+
- React-NativeModulesApple
1300+
- React-RCTFabric
1301+
- React-rendererdebug
1302+
- React-utils
1303+
- ReactCodegen
1304+
- ReactCommon/turbomodule/bridging
1305+
- ReactCommon/turbomodule/core
1306+
- Yoga
12861307
- React-nativeconfig (0.75.2)
12871308
- React-NativeModulesApple (0.75.2):
12881309
- glog
@@ -1697,6 +1718,7 @@ DEPENDENCIES:
16971718
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
16981719
- "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)"
16991720
- "react-native-slider (from `../../../node_modules/@react-native-community/slider`)"
1721+
- react-native-wgpu (from `../../../node_modules/react-native-wgpu`)
17001722
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
17011723
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
17021724
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -1811,6 +1833,8 @@ EXTERNAL SOURCES:
18111833
:path: "../../../node_modules/@shopify/react-native-skia"
18121834
react-native-slider:
18131835
:path: "../../../node_modules/@react-native-community/slider"
1836+
react-native-wgpu:
1837+
:path: "../../../node_modules/react-native-wgpu"
18141838
React-nativeconfig:
18151839
:path: "../../../node_modules/react-native/ReactCommon"
18161840
React-NativeModulesApple:
@@ -1913,6 +1937,7 @@ SPEC CHECKSUMS:
19131937
react-native-safe-area-context: ab8f4a3d8180913bd78ae75dd599c94cce3d5e9a
19141938
react-native-skia: b1f33ae82bb728aee1d1602a6adfaee52af43f61
19151939
react-native-slider: 97ce0bd921f40de79cead9754546d5e4e7ba44f8
1940+
react-native-wgpu: fb9d60b0f4c63a03fb60bd986d758aeffca6ee13
19161941
React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d
19171942
React-NativeModulesApple: 7ff2e2cfb2e5fa5bdedcecf28ce37e696c6ef1e1
19181943
React-perflogger: 8a360ccf603de6ddbe9ff8f54383146d26e6c936

apps/paper/ios/paper.xcodeproj/xcshareddata/xcschemes/paper.xcscheme

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
ignoresPersistentStateOnLaunch = "NO"
5050
debugDocumentVersioning = "YES"
5151
debugServiceExtension = "internal"
52+
enableGPUValidationMode = "1"
5253
allowLocationSimulation = "YES">
5354
<BuildableProductRunnable
5455
runnableDebuggingMode = "0">

apps/paper/jestSetup.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ jest.mock("expo-asset", () => ({
1414
useAssets: () => [[], undefined],
1515
}));
1616

17+
jest.mock("react-native-wgpu", () => {
18+
return {
19+
Canvas: jest.fn(),
20+
};
21+
});
22+
1723
jest.mock("react-native-reanimated", () => {
1824
// The mock for `call` immediately calls the callback which is incorrect
1925
// So we override it with a no-op

apps/paper/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"react-native-safe-area-context": "^4.10.9",
3333
"react-native-screens": "^3.34.0",
3434
"react-native-svg": "^15.6.0",
35+
"react-native-wgpu": "^0.1.7",
3536
"typescript": "^5.2.2"
3637
},
3738
"devDependencies": {

apps/paper/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
FrostedCard,
3131
SpeedTest,
3232
Video,
33+
WebGPU,
3334
} from "./Examples";
3435
import { CI, Tests } from "./Tests";
3536
import { HomeScreen } from "./Home";
@@ -108,6 +109,14 @@ const App = () => {
108109
title: "🎨 Skia",
109110
}}
110111
/>
112+
<Stack.Screen
113+
name="WebGPU"
114+
key="WebGPU"
115+
component={WebGPU}
116+
options={{
117+
title: "🏔️ WebGPU",
118+
}}
119+
/>
111120
<Stack.Screen
112121
key="Tests"
113122
name="Tests"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react";
2+
import { StyleSheet, View } from "react-native";
3+
import { useFrameCallback } from "react-native-reanimated";
4+
import { Canvas } from "react-native-wgpu";
5+
6+
import { useLoop } from "../../components/Animations";
7+
8+
import { drawBreatheDemo, useSkiaContext } from "./utils";
9+
10+
export function WebGPU() {
11+
const { ref, context } = useSkiaContext();
12+
13+
const progress = useLoop({ duration: 3000 });
14+
15+
useFrameCallback(() => {
16+
if (!context.value) {
17+
return;
18+
}
19+
20+
const ctx = context.value;
21+
drawBreatheDemo(ctx, progress.value);
22+
ctx.present();
23+
});
24+
25+
return (
26+
<View style={style.container}>
27+
<Canvas ref={ref} style={style.webgpu} />
28+
</View>
29+
);
30+
}
31+
32+
const style = StyleSheet.create({
33+
container: {
34+
flex: 1,
35+
},
36+
webgpu: {
37+
flex: 1,
38+
},
39+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./WebGPU";
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { SkiaContext } from "@shopify/react-native-skia";
2+
import {
3+
BlendMode,
4+
BlurStyle,
5+
mix,
6+
polar2Canvas,
7+
Skia,
8+
} from "@shopify/react-native-skia";
9+
import { Dimensions, PixelRatio } from "react-native";
10+
import { useSharedValue } from "react-native-reanimated";
11+
import { useCanvasEffect } from "react-native-wgpu";
12+
13+
export const useSkiaContext = () => {
14+
const context = useSharedValue<SkiaContext | null>(null);
15+
const ref = useCanvasEffect(() => {
16+
const nativeSurface = ref.current!.getNativeSurface();
17+
context.value = Skia.Context(
18+
nativeSurface.surface,
19+
nativeSurface.width * pd,
20+
nativeSurface.height * pd
21+
);
22+
});
23+
return {
24+
context,
25+
ref,
26+
};
27+
};
28+
29+
const pd = PixelRatio.get();
30+
const { width, height } = Dimensions.get("window");
31+
const center = { x: width / 2, y: height / 2 };
32+
const R = width / 4;
33+
34+
const c1 = "#61bea2";
35+
const c2 = "#529ca0";
36+
const root = Skia.Paint();
37+
root.setBlendMode(BlendMode.Screen);
38+
root.setMaskFilter(Skia.MaskFilter.MakeBlur(BlurStyle.Solid, 10, true));
39+
const p1 = root.copy();
40+
p1.setColor(Skia.Color(c1));
41+
const p2 = root.copy();
42+
p2.setColor(Skia.Color(c2));
43+
44+
export const drawBreatheDemo = (ctx: SkiaContext, progress: number) => {
45+
"worklet";
46+
const surface = ctx.getSurface();
47+
const canvas = surface.getCanvas();
48+
canvas.clear(Skia.Color("rgb(36, 43, 56)"));
49+
canvas.save();
50+
canvas.scale(pd, pd);
51+
canvas.rotate(progress * -180, center.x, center.y);
52+
// const offscreen = Skia.Surface.MakeOffscreen(256, 256)!;
53+
// const offscreenCanvas = offscreen.getCanvas();
54+
// offscreenCanvas.clear(Skia.Color("green"));
55+
// canvas.drawImage(offscreen.makeImageSnapshot(), 0, 0);
56+
new Array(6).fill(0).map((_, index) => {
57+
canvas.save();
58+
const theta = (index * (2 * Math.PI)) / 6;
59+
const { x, y } = polar2Canvas(
60+
{ theta, radius: progress * R },
61+
{ x: 0, y: 0 }
62+
);
63+
const scale = mix(progress, 0.3, 1);
64+
65+
canvas.translate(center.x, center.y);
66+
canvas.translate(x, y);
67+
canvas.scale(scale, scale);
68+
canvas.translate(-center.x, -center.y);
69+
70+
const paint = index % 2 ? p1 : p2;
71+
canvas.drawCircle(center.x, center.y, R, paint);
72+
canvas.restore();
73+
});
74+
75+
canvas.restore();
76+
};

apps/paper/src/Examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export * from "./Stickers";
2424
export * from "./FrostedCard";
2525
export * from "./SpeedTest";
2626
export * from "./Video";
27+
export * from "./WebGPU";

apps/paper/src/Home/HomeScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const HomeScreen = () => {
1818
route="API"
1919
testId="API"
2020
/>
21+
<HomeScreenButton title="🏔️ WebGPU" description="WebGPU" route="WebGPU" />
2122
<HomeScreenButton
2223
title="🎥 Reanimated"
2324
description="Reanimated & Gesture Handler"

0 commit comments

Comments
 (0)