@typegpu/react 0.10.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Software Mansion <swmansion.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ <div align="center">
2
+
3
+ # @typegpu/react
4
+
5
+ 🚧 **Under Construction** 🚧
6
+
7
+ </div>
8
+
9
+ # Basic usage
10
+
11
+ ```ts
12
+ import { d, common } from 'typegpu';
13
+ import { hsvToRgb } from '@typegpu/color';
14
+ import { useFrame, useRoot, useUniformValue, useMirroredUniform, useConfigureContext } from '@typegpu/react';
15
+
16
+ const App = (props: Props) => {
17
+ const time = useUniformValue(d.f32, 0);
18
+ const color = useMirroredUniform(d.vec3f, props.color);
19
+
20
+ const root = useRoot();
21
+ const renderPipeline = useMemo(() => root.createRenderPipeline({
22
+ vertex: common.fullScreenTriangle,
23
+ // Runs each frame on the GPU 🌈
24
+ fragment: ({ uv }) => {
25
+ 'use gpu';
26
+ return hsvToRgb(time.$, uv.x, uv.y) * color.$;
27
+ },
28
+ }), [root, time, color]);
29
+
30
+ const { canvasRefCallback, ctxRef } = useConfigureContext();
31
+
32
+ // Runs each frame on the CPU 🤖
33
+ useFrame(({ elapsedSeconds }) => {
34
+ if (!ctxRef.current) return;
35
+
36
+ time.value = elapsedSeconds;
37
+ renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3);
38
+ });
39
+
40
+ return <canvas ref={canvasRefCallback} />;
41
+ };
42
+ ```
package/index.d.mts ADDED
@@ -0,0 +1,92 @@
1
+ import { LayoutEntryToInput, TgpuBindGroup, TgpuBindGroupLayout, TgpuBuffer, TgpuMutable, TgpuRoot, ValidateBufferSchema, ValidateStorageSchema, ValidateUniformSchema, d } from "typegpu";
2
+ import * as d$1 from "typegpu/data";
3
+
4
+ //#region src/root-context.d.ts
5
+ declare function useRoot(): TgpuRoot;
6
+ //#endregion
7
+ //#region src/use-frame.d.ts
8
+ interface FrameCtx {
9
+ /**
10
+ * Time elapsed since the last frame
11
+ */
12
+ readonly deltaSeconds: number;
13
+ /**
14
+ * Time elapsed since the mounting of this hook
15
+ */
16
+ readonly elapsedSeconds: number;
17
+ }
18
+ declare function useFrame(cb: (ctx: FrameCtx) => void): void;
19
+ //#endregion
20
+ //#region src/use-mutable.d.ts
21
+ declare function useMutable<TSchema extends d.AnyWgslData>(schema: ValidateStorageSchema<TSchema>, initialValue?: d.Infer<TSchema>): TgpuMutable<TSchema>;
22
+ //#endregion
23
+ //#region src/symbols.d.ts
24
+ declare const $buffer: unique symbol;
25
+ //#endregion
26
+ //#region src/use-uniform-value.d.ts
27
+ interface UniformValue<TSchema extends d$1.BaseData, TValue extends d$1.Infer<TSchema>> {
28
+ schema: TSchema;
29
+ value: TValue;
30
+ readonly $: d$1.InferGPU<TSchema>;
31
+ readonly [$buffer]: TgpuBuffer<TSchema>;
32
+ }
33
+ declare function useUniformValue<TSchema extends d$1.AnyWgslData, TValue extends d$1.Infer<TSchema>>(schema: ValidateUniformSchema<TSchema>, initialValue?: TValue): UniformValue<TSchema, TValue>;
34
+ //#endregion
35
+ //#region src/use-buffer.d.ts
36
+ interface UseBufferOptions<TSchema extends d$1.AnyData> {
37
+ initial?: (() => d$1.Infer<NoInfer<TSchema>>) | d$1.Infer<NoInfer<TSchema>>;
38
+ onInit?: (buffer: TgpuBuffer<TSchema>) => void;
39
+ }
40
+ declare function useBuffer<TSchema extends d$1.AnyData>(schema: ValidateBufferSchema<TSchema>, options?: UseBufferOptions<TSchema>): TgpuBuffer<TSchema>;
41
+ //#endregion
42
+ //#region src/use-configure-context.d.ts
43
+ interface UseConfigureContextOptions {
44
+ /**
45
+ * @default true
46
+ */
47
+ autoResize?: boolean;
48
+ }
49
+ interface UseConfigureContextResult {
50
+ canvasRefCallback: React.RefCallback<HTMLCanvasElement>;
51
+ canvasRef: Readonly<React.Ref<HTMLCanvasElement | null>>;
52
+ ctxRef: React.RefObject<GPUCanvasContext | null>;
53
+ }
54
+ declare function useConfigureContext(options?: UseConfigureContextOptions): UseConfigureContextResult;
55
+ //#endregion
56
+ //#region src/use-bind-group.d.ts
57
+ type ExtractInputFromEntries<T extends TgpuBindGroupLayout['entries']> = { [Key in keyof T]: T[Key] extends {
58
+ uniform: d.BaseData;
59
+ } ? LayoutEntryToInput<T[Key]> | UniformValue<T[Key]['uniform'], d.Infer<T[Key]['uniform']>> : LayoutEntryToInput<T[Key]> };
60
+ /**
61
+ * Creates a group of resources that can be bound to a shader based on a specified layout.
62
+ *
63
+ * @remarks
64
+ * Typed wrapper around a GPUBindGroup.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const fooLayout = tgpu.bindGroupLayout({
69
+ * foo: { uniform: d.vec3f },
70
+ * bar: { texture: 'float' },
71
+ * });
72
+ *
73
+ * function App() {
74
+ * const fooBuffer = useBuffer(...);
75
+ * const barTexture = useTexture(...);
76
+ *
77
+ * const fooBindGroup = useBindGroup(fooLayout, {
78
+ * foo: fooBuffer,
79
+ * bar: barTexture,
80
+ * });
81
+ *
82
+ * // ...
83
+ * }
84
+ * ```
85
+ *
86
+ * @param layout Layout describing the bind group to be created.
87
+ * @param entries A record with values being the resources populating the bind group
88
+ * and keys being their associated names, matching the layout keys.
89
+ */
90
+ declare function useBindGroup<TLayout extends TgpuBindGroupLayout>(layout: TLayout, entries: ExtractInputFromEntries<TLayout['entries']>): TgpuBindGroup<TLayout['entries']>;
91
+ //#endregion
92
+ export { type UniformValue, useBindGroup, useBuffer, useConfigureContext, useFrame, useMutable, useRoot, useUniformValue };
package/index.mjs ADDED
@@ -0,0 +1,269 @@
1
+ import React, { createContext, use, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import tgpu from "typegpu";
3
+ import "react/jsx-runtime";
4
+
5
+ //#region src/root-context.tsx
6
+ var RootContext = class {
7
+ #root;
8
+ #rootPromise;
9
+ initOrGetRoot() {
10
+ if (this.#root) return this.#root;
11
+ if (!this.#rootPromise) this.#rootPromise = tgpu.init().then((root) => {
12
+ this.#root = root;
13
+ return root;
14
+ });
15
+ return this.#rootPromise;
16
+ }
17
+ };
18
+ /**
19
+ * Used in case no provider is mounted
20
+ */
21
+ const globalRootContextValue = new RootContext();
22
+ const rootContext = createContext(null);
23
+ function useRoot() {
24
+ const maybeRoot = (useContext(rootContext) ?? globalRootContextValue).initOrGetRoot();
25
+ return maybeRoot instanceof Promise ? use(maybeRoot) : maybeRoot;
26
+ }
27
+
28
+ //#endregion
29
+ //#region src/use-frame.ts
30
+ function useFrame(cb) {
31
+ const latestCb = useRef(cb);
32
+ useEffect(() => {
33
+ latestCb.current = cb;
34
+ }, [cb]);
35
+ useEffect(() => {
36
+ let frameId;
37
+ let startTime;
38
+ let lastTime;
39
+ const loop = () => {
40
+ frameId = requestAnimationFrame(loop);
41
+ const now = performance.now();
42
+ if (lastTime === void 0 || startTime === void 0) {
43
+ startTime = now;
44
+ lastTime = now;
45
+ }
46
+ latestCb.current({
47
+ deltaSeconds: (now - lastTime) / 1e3,
48
+ elapsedSeconds: (now - startTime) / 1e3
49
+ });
50
+ lastTime = now;
51
+ };
52
+ loop();
53
+ return () => {
54
+ if (frameId !== void 0) cancelAnimationFrame(frameId);
55
+ };
56
+ }, []);
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/use-mutable.ts
61
+ function useMutable(schema, initialValue) {
62
+ const root = useRoot();
63
+ const [mutable] = useState(() => {
64
+ return root.createMutable(schema, initialValue);
65
+ });
66
+ const cleanupRef = useRef(null);
67
+ useEffect(() => {
68
+ if (cleanupRef.current) clearTimeout(cleanupRef.current);
69
+ return () => {
70
+ cleanupRef.current = setTimeout(() => {
71
+ mutable.buffer.destroy();
72
+ }, 200);
73
+ };
74
+ }, [mutable]);
75
+ return mutable;
76
+ }
77
+
78
+ //#endregion
79
+ //#region package.json
80
+ var version = "0.10.0-alpha.1";
81
+
82
+ //#endregion
83
+ //#region src/symbols.ts
84
+ const $buffer = Symbol(`@typegpu/react:${version}:$buffer`);
85
+
86
+ //#endregion
87
+ //#region src/use-uniform-value.ts
88
+ function initialValueFromSchema(schema) {
89
+ if (typeof schema !== "function") throw new Error("Cannot use a non-callable schema with `useUniformValue`");
90
+ return schema();
91
+ }
92
+ function useUniformValue(schema, initialValue) {
93
+ const root = useRoot();
94
+ const [uniformBuffer] = useState(() => {
95
+ return root.createUniform(schema, initialValue);
96
+ });
97
+ const cleanupRef = useRef(null);
98
+ useEffect(() => {
99
+ if (cleanupRef.current) clearTimeout(cleanupRef.current);
100
+ return () => {
101
+ cleanupRef.current = setTimeout(() => {
102
+ uniformBuffer.buffer.destroy();
103
+ }, 200);
104
+ };
105
+ }, [uniformBuffer]);
106
+ return useMemo(() => {
107
+ let currentValue = initialValue ?? initialValueFromSchema(schema);
108
+ return {
109
+ schema,
110
+ [$buffer]: uniformBuffer.buffer,
111
+ get value() {
112
+ return currentValue;
113
+ },
114
+ set value(newValue) {
115
+ currentValue = newValue;
116
+ uniformBuffer.write(newValue);
117
+ },
118
+ get $() {
119
+ return uniformBuffer.$;
120
+ }
121
+ };
122
+ }, []);
123
+ }
124
+
125
+ //#endregion
126
+ //#region src/use-buffer.ts
127
+ function useBuffer(schema, options) {
128
+ const { initial, onInit } = options ?? {};
129
+ const root = useRoot();
130
+ const [buffer] = useState(() => {
131
+ const buffer = root.createBuffer(schema, typeof initial === "function" ? initial() : initial);
132
+ onInit?.(buffer);
133
+ return buffer;
134
+ });
135
+ const cleanupRef = useRef(null);
136
+ useEffect(() => {
137
+ if (cleanupRef.current) clearTimeout(cleanupRef.current);
138
+ return () => {
139
+ cleanupRef.current = setTimeout(() => {
140
+ buffer.buffer.destroy();
141
+ }, 200);
142
+ };
143
+ }, [buffer]);
144
+ return buffer;
145
+ }
146
+
147
+ //#endregion
148
+ //#region src/use-effect-event.ts
149
+ /**
150
+ * Polyfill for the `useEffectEvent` React hook.
151
+ * WARNING: Do not use in the render phase, nor in `useLayoutEffect` calls.
152
+ * @param handler
153
+ * @returns A stable reference of the passed in function.
154
+ */
155
+ function useEffectEvent(handler) {
156
+ const handlerRef = useRef(handler);
157
+ useLayoutEffect(() => {
158
+ handlerRef.current = handler;
159
+ });
160
+ return useCallback((...args) => {
161
+ const fn = handlerRef.current;
162
+ return fn(...args);
163
+ }, []);
164
+ }
165
+ var use_effect_event_default = React.useEffectEvent ?? useEffectEvent;
166
+
167
+ //#endregion
168
+ //#region src/use-configure-context.ts
169
+ function useConfigureContext(options) {
170
+ const { autoResize = true } = options ?? {};
171
+ const root = useRoot();
172
+ const canvasRef = useRef(null);
173
+ const resizeEffect = use_effect_event_default((entries) => {
174
+ const entry = entries[0];
175
+ if (!canvasRef.current || !entry) return;
176
+ const el = canvasRef.current;
177
+ const dpcb = entry.devicePixelContentBoxSize?.[0];
178
+ const dpr = dpcb ? 1 : window.devicePixelRatio || 1;
179
+ const box = dpcb ?? (Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize);
180
+ if (!box) return;
181
+ el.width = Math.round(box.inlineSize * dpr);
182
+ el.height = Math.round(box.blockSize * dpr);
183
+ });
184
+ const ctxRef = useRef(null);
185
+ const resizeObserverRef = useRef(null);
186
+ const canvasRefCallback = useCallback((el) => {
187
+ if (el) {
188
+ canvasRef.current = el;
189
+ ctxRef.current = root.configureContext({
190
+ canvas: el,
191
+ alphaMode: "premultiplied"
192
+ });
193
+ if (autoResize) {
194
+ if (resizeObserverRef.current) resizeObserverRef.current.disconnect();
195
+ resizeObserverRef.current = new ResizeObserver(resizeEffect);
196
+ resizeObserverRef.current.observe(el);
197
+ }
198
+ } else {
199
+ canvasRef.current = null;
200
+ ctxRef.current = null;
201
+ }
202
+ return () => {
203
+ canvasRef.current = null;
204
+ ctxRef.current = null;
205
+ resizeObserverRef.current?.disconnect();
206
+ resizeObserverRef.current = null;
207
+ };
208
+ }, [root]);
209
+ useEffect(() => {
210
+ const el = canvasRef.current;
211
+ if (!el) return;
212
+ if (resizeObserverRef.current) resizeObserverRef.current.disconnect();
213
+ if (autoResize) {
214
+ resizeObserverRef.current = new ResizeObserver(resizeEffect);
215
+ resizeObserverRef.current.observe(el);
216
+ }
217
+ return () => {
218
+ resizeObserverRef.current?.disconnect();
219
+ resizeObserverRef.current = null;
220
+ };
221
+ }, [resizeEffect]);
222
+ return {
223
+ canvasRefCallback,
224
+ canvasRef,
225
+ ctxRef
226
+ };
227
+ }
228
+
229
+ //#endregion
230
+ //#region src/use-bind-group.ts
231
+ /**
232
+ * Creates a group of resources that can be bound to a shader based on a specified layout.
233
+ *
234
+ * @remarks
235
+ * Typed wrapper around a GPUBindGroup.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const fooLayout = tgpu.bindGroupLayout({
240
+ * foo: { uniform: d.vec3f },
241
+ * bar: { texture: 'float' },
242
+ * });
243
+ *
244
+ * function App() {
245
+ * const fooBuffer = useBuffer(...);
246
+ * const barTexture = useTexture(...);
247
+ *
248
+ * const fooBindGroup = useBindGroup(fooLayout, {
249
+ * foo: fooBuffer,
250
+ * bar: barTexture,
251
+ * });
252
+ *
253
+ * // ...
254
+ * }
255
+ * ```
256
+ *
257
+ * @param layout Layout describing the bind group to be created.
258
+ * @param entries A record with values being the resources populating the bind group
259
+ * and keys being their associated names, matching the layout keys.
260
+ */
261
+ function useBindGroup(layout, entries) {
262
+ return useRoot().createBindGroup(layout, Object.fromEntries(Object.entries(entries).map(([key, value]) => {
263
+ if (value[$buffer]) return [key, value[$buffer]];
264
+ return [key, value];
265
+ })));
266
+ }
267
+
268
+ //#endregion
269
+ export { useBindGroup, useBuffer, useConfigureContext, useFrame, useMutable, useRoot, useUniformValue };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@typegpu/react",
3
+ "version": "0.10.0-alpha.1",
4
+ "description": "The best way to integrate TypeGPU into your React app.",
5
+ "keywords": [],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "types": "./index.d.ts",
10
+ "exports": {
11
+ "./package.json": "./package.json",
12
+ ".": {
13
+ "types": "./index.d.mts",
14
+ "default": "./index.mjs"
15
+ }
16
+ },
17
+ "peerDependencies": {
18
+ "react": "^19.0.0",
19
+ "typegpu": "^0.10.2"
20
+ },
21
+ "main": "./index.js",
22
+ "private": false,
23
+ "dependencies": {},
24
+ "scripts": {}
25
+ }