Skip to content
Draft
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
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/examples/exampleContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const metaFiles = R.pipe(
);

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

const tsFilesImportFunctions = R.pipe(
import.meta.glob('./**/index.ts') as Record<
import.meta.glob(['./**/index.ts', './**/index.tsx']) as Record<
string,
() => Promise<unknown>
>,
Expand Down
1 change: 1 addition & 0 deletions apps/typegpu-docs/src/examples/react/triangle/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="example-app"></div>
40 changes: 40 additions & 0 deletions apps/typegpu-docs/src/examples/react/triangle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as d from 'typegpu/data';
import { useFrame, useRender, useUniformValue } from '@typegpu/react';
import { hsvToRgb } from '@typegpu/color';

function App() {
const time = useUniformValue(d.f32, 0);

useFrame(() => {
time.value = performance.now() / 1000;
});

const { ref } = useRender({
fragment: () => {
'kernel';
const t = time.$;
const rgb = hsvToRgb(d.vec3f(t * 0.5, 1, 1));
return d.vec4f(rgb, 1);
},
});

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() {
setTimeout(() => reactRoot.unmount(), 0);
}

// #endregion
5 changes: 5 additions & 0 deletions apps/typegpu-docs/src/examples/react/triangle/meta.json
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.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div class='result'>Wait for the tests to finish running...</div>;
<div class="result">Wait for the tests to finish running...</div>;
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'],
};
4 changes: 2 additions & 2 deletions apps/typegpu-docs/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createJiti } from 'jiti';
import { defineConfig } from 'vitest/config';
import { defineConfig, type Plugin } from 'vitest/config';
import { imagetools } from 'vite-imagetools';
import type TypeGPUPlugin from 'unplugin-typegpu/vite';

Expand All @@ -13,7 +13,7 @@ export default defineConfig({
plugins: [
typegpu({ include: [/\.m?[jt]sx?/] }),
/** @type {any} */ imagetools(),
],
] as Plugin[],
server: {
proxy: {
'/TypeGPU': {
Expand Down
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