Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/typegpu-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@typegpu/color": "workspace:*",
"@typegpu/noise": "workspace:*",
"@typegpu/sdf": "workspace:*",
"@typegpu/react": "workspace:*",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"arktype": "catalog:",
Expand Down
12 changes: 8 additions & 4 deletions apps/typegpu-docs/src/components/ExampleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,21 @@ function useExample(
export function ExampleView({ example }: Props) {
const { tsFiles, tsImport, htmlFile } = example;

const filePaths = tsFiles.map((file) => file.path);
const entryFile = filePaths.find((path) =>
path.startsWith('index.ts')
) as string;

const [snackbarText, setSnackbarText] = useAtom(currentSnackbarAtom);
const [currentFilePath, setCurrentFilePath] = useState<string>('index.ts');
const [currentFilePath, setCurrentFilePath] = useState(entryFile);

const codeEditorShowing = useAtomValue(codeEditorShownAtom);
const codeEditorMobileShowing = useAtomValue(codeEditorShownMobileAtom);
const exampleHtmlRef = useRef<HTMLDivElement>(null);

const filePaths = tsFiles.map((file) => file.path);
const editorTabsList = [
'index.ts',
...filePaths.filter((name) => name !== 'index.ts'),
entryFile,
...filePaths.filter((name) => name !== entryFile),
'index.html',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import typegpuNoisePackageJson from '@typegpu/noise/package.json' with {
import typegpuSdfPackageJson from '@typegpu/sdf/package.json' with {
type: 'json',
};
import typegpuPackageJson from 'typegpu/package.json' with { type: 'json' };
import typegpuReactPackageJson from '@typegpu/react/package.json' with {
type: 'json',
};
import typegpuPackageJson from 'typegpu/package.json' with {
type: 'json',
};
import unpluginPackageJson from 'unplugin-typegpu/package.json' with {
type: 'json',
};
Expand Down Expand Up @@ -128,6 +133,7 @@ ${example.htmlFile.content}
"@typegpu/noise": "${typegpuNoisePackageJson.version}",
"@typegpu/color": "${typegpuColorPackageJson.version}",
"@typegpu/sdf": "${typegpuSdfPackageJson.version}"
"@typegpu/react": "${typegpuReactPackageJson.version}"
}
}`,
'vite.config.js': `\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="example-app"></div>
45 changes: 45 additions & 0 deletions apps/typegpu-docs/src/content/examples/react/triangle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as d from 'typegpu/data';
import { vec4f } from 'typegpu/data';
import { useRender } from '@typegpu/react';

function App() {
const { ref } = useRender({
vertex: ({ vertexIndex }) => {
'kernel';
const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)];
const uv = [d.vec2f(0, 1), d.vec2f(2, 1), d.vec2f(0, -1)];

return {
pos: d.vec4f(pos[vertexIndex] as d.v2f, 0, 1),
uv: uv[vertexIndex] as d.v2f,
};
},
fragment: ({ uv }) => {
'kernel';
return vec4f(uv.x, uv.y, 1, 1);
},
});

// TODO: Provide a time variable to the shader with useUniformValue
// TODO: Make the gradient shift colors over time using hsvToRgb from @typegpu/color

return (
<main>
<canvas ref={ref} width='256' height='256' />
</main>
);
}

// #region Example controls and cleanup

import { createRoot } from 'react-dom/client';
const reactRoot = createRoot(
document.getElementById('example-app') as HTMLDivElement,
);
reactRoot.render(<App />);

export function onCleanup() {
reactRoot.unmount();
}

// #endregion
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "React: Spinning Triangle",
"category": "react",
"tags": ["experimental"]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 8 additions & 2 deletions apps/typegpu-docs/src/utils/examples/exampleContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ const metaFiles = R.pipe(
);

const readonlyTsFiles = R.pipe(
import.meta.glob('../../content/examples/**/*.ts', {
import.meta.glob([
'../../content/examples/**/*.ts',
'../../content/examples/**/*.tsx',
], {
query: 'raw',
eager: true,
import: 'default',
Expand All @@ -91,7 +94,10 @@ const readonlyTsFiles = R.pipe(
);

const tsFilesImportFunctions = R.pipe(
import.meta.glob('../../content/examples/**/index.ts') as Record<
import.meta.glob([
'../../content/examples/**/index.ts',
'../../content/examples/**/index.tsx',
]) as Record<
string,
() => Promise<unknown>
>,
Expand Down
25 changes: 25 additions & 0 deletions apps/typegpu-docs/src/utils/examples/sandboxModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ const allPackagesSrcFiles = pipe(
fromEntries(),
);

const reactModules = pipe(
entries(
import.meta.glob(
'../../../node_modules/@types/react/**/*.d.ts',
{
query: 'raw',
eager: true,
import: 'default',
},
) as Record<string, string>,
),
map((dtsFile) => dtsFileToModule(dtsFile, '../../../node_modules/')),
fromEntries(),
);

const mediacaptureModules = pipe(
entries(
import.meta.glob(
Expand All @@ -54,7 +69,11 @@ const mediacaptureModules = pipe(
export const SANDBOX_MODULES: Record<string, SandboxModuleDefinition> = {
...allPackagesSrcFiles,
...mediacaptureModules,
...reactModules,

'react': {
typeDef: { reroute: ['@types/react/index.d.ts'] },
},
'@webgpu/types': {
typeDef: { content: dtsWebGPU },
},
Expand All @@ -78,4 +97,10 @@ export const SANDBOX_MODULES: Record<string, SandboxModuleDefinition> = {
'@typegpu/color': {
typeDef: { reroute: ['typegpu-color/src/index.ts'] },
},
'@typegpu/sdf': {
typeDef: { reroute: ['typegpu-sdf/src/index.ts'] },
},
'@typegpu/react': {
typeDef: { reroute: ['typegpu-react/src/index.ts'] },
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export const tsCompilerOptions: languages.typescript.CompilerOptions = {
skipLibCheck: true,
exactOptionalPropertyTypes: true,
baseUrl: '.',
jsx: languages.typescript.JsxEmit.React,
lib: ['dom', 'es2021'],
};
34 changes: 34 additions & 0 deletions packages/typegpu-react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<div align="center">

# @typegpu/react

🚧 **Under Construction** 🚧

</div>

# Basic usage (draft)

```ts
import { hsvToRgb } from '@typegpu/color';
import { useFrame, useRender, useUniformValue } from '@typegpu/react';

const App = (props: Props) => {
const time = useUniformValue(d.f32, 0);
const color = useMirroredUniform(d.vec3f, props.color);

// Runs each frame on the CPU 🤖
useFrame(() => {
time.value = performance.now() / 1000;
});

const { ref } = useRender({
// Runs each frame on the GPU 🌈
fragment: ({ uv }) => {
'kernel';
return hsvToRgb(time.$, uv.x, uv.y) * color.$;
},
});

return <canvas ref={ref} />;
};
```
7 changes: 7 additions & 0 deletions packages/typegpu-react/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"exclude": ["."],
"fmt": {
"exclude": ["!.", "./dist"],
"singleQuote": true
}
}
45 changes: 45 additions & 0 deletions packages/typegpu-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@typegpu/react",
"type": "module",
"version": "0.7.0",
"description": "The best way to integrate TypeGPU into your React app.",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"publishConfig": {
"directory": "dist",
"linkDirectory": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./dist/package.json",
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"sideEffects": false,
"scripts": {
"build": "tsdown",
"test:types": "pnpm tsc --p ./tsconfig.json --noEmit",
"prepublishOnly": "tgpu-dev-cli prepack"
},
"keywords": [],
"license": "MIT",
"peerDependencies": {
"typegpu": "^0.7.0",
"react": "^19.0.0"
},
"devDependencies": {
"@typegpu/tgpu-dev-cli": "workspace:*",
"@webgpu/types": "catalog:types",
"@types/react": "^19.0.0",
"tsdown": "catalog:build",
"typegpu": "workspace:*",
"typescript": "catalog:types",
"unplugin-typegpu": "workspace:*"
}
}
3 changes: 3 additions & 0 deletions packages/typegpu-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { useFrame } from './use-frame.ts';
export { useRender } from './use-render.ts';
export { useUniformValue } from './use-uniform-value.ts';
56 changes: 56 additions & 0 deletions packages/typegpu-react/src/root-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
createContext,
type ReactNode,
use,
useContext,
useState,
} from 'react';
import tgpu, { type TgpuRoot } from 'typegpu';

class RootContext {
#root: TgpuRoot | undefined;
#rootPromise: Promise<TgpuRoot> | undefined;

initOrGetRoot(): Promise<TgpuRoot> | TgpuRoot {
if (this.#root) {
return this.#root;
}

if (!this.#rootPromise) {
this.#rootPromise = tgpu.init().then((root) => {
this.#root = root;
return root;
});
}

return this.#rootPromise;
}
}

/**
* Used in case no provider is mounted
*/
const globalRootContextValue = new RootContext();

const rootContext = createContext<RootContext | null>(null);

export interface RootProps {
children?: ReactNode | undefined;
}

export const Root = ({ children }: RootProps) => {
const [ctx] = useState(() => new RootContext());

return (
<rootContext.Provider value={ctx}>
{children}
</rootContext.Provider>
);
};

export function useRoot(): TgpuRoot {
const context = useContext(rootContext) ?? globalRootContextValue;

const maybeRoot = context.initOrGetRoot();
return maybeRoot instanceof Promise ? use(maybeRoot) : maybeRoot;
}
26 changes: 26 additions & 0 deletions packages/typegpu-react/src/use-frame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';

export function useFrame(cb: () => void) {
const latestCb = useRef(cb);

useEffect(() => {
latestCb.current = cb;
}, [cb]);

useEffect(() => {
let frameId: number | undefined;

const loop = () => {
frameId = requestAnimationFrame(loop);
latestCb.current();
};

loop();

return () => {
if (frameId !== undefined) {
cancelAnimationFrame(frameId);
}
};
}, []);
}
Loading
Loading