@fjandin/react-shader 0.0.2 → 0.0.4
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/README.md +147 -0
- package/dist/ReactShader.d.ts +1 -1
- package/dist/ReactShader.d.ts.map +1 -1
- package/dist/example/frontend.d.ts.map +1 -1
- package/dist/index.cjs +9 -62
- package/dist/index.js +10 -63
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# @fjandin/react-shader
|
|
2
|
+
|
|
3
|
+
A React component for rendering WebGL fragment and vertex shaders. Designed to work with Shadertoy-style shaders with automatic uniform handling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fjandin/react-shader
|
|
9
|
+
# or
|
|
10
|
+
yarn add @fjandin/react-shader
|
|
11
|
+
# or
|
|
12
|
+
bun add @fjandin/react-shader
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { ReactShader } from "@fjandin/react-shader"
|
|
19
|
+
|
|
20
|
+
const fragment = `#version 300 es
|
|
21
|
+
precision highp float;
|
|
22
|
+
|
|
23
|
+
uniform float iTime;
|
|
24
|
+
uniform vec2 iResolution;
|
|
25
|
+
|
|
26
|
+
out vec4 fragColor;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
vec2 uv = gl_FragCoord.xy / iResolution;
|
|
30
|
+
vec3 color = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
|
|
31
|
+
fragColor = vec4(color, 1.0);
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
function App() {
|
|
36
|
+
return (
|
|
37
|
+
<div style={{ width: "800px", height: "600px" }}>
|
|
38
|
+
<ReactShader fragment={fragment} />
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
| Prop | Type | Required | Default | Description |
|
|
47
|
+
|------|------|----------|---------|-------------|
|
|
48
|
+
| `fragment` | `string` | Yes | - | GLSL fragment shader source code |
|
|
49
|
+
| `vertex` | `string` | No | Default quad shader | GLSL vertex shader source code |
|
|
50
|
+
| `uniforms` | `Record<string, UniformValue>` | No | `{}` | Custom uniform values |
|
|
51
|
+
| `className` | `string` | No | - | CSS class name for the container |
|
|
52
|
+
| `debug` | `boolean` | No | `false` | Show debug overlay with resolution and mouse info |
|
|
53
|
+
| `fullscreen` | `boolean` | No | `false` | Render as fixed fullscreen overlay |
|
|
54
|
+
| `timeScale` | `number` | No | `1` | Scale factor for elapsed time |
|
|
55
|
+
| `onFrame` | `(info: FrameInfo) => void` | No | - | Callback invoked on each frame |
|
|
56
|
+
|
|
57
|
+
## Built-in Uniforms
|
|
58
|
+
|
|
59
|
+
These uniforms are automatically provided to your shader every frame:
|
|
60
|
+
|
|
61
|
+
| Uniform | GLSL Type | Description |
|
|
62
|
+
|---------|-----------|-------------|
|
|
63
|
+
| `iTime` | `float` | Elapsed time in seconds (scaled by `timeScale` prop) |
|
|
64
|
+
| `iMouse` | `vec2` | Mouse position in pixels (Y=0 at bottom) |
|
|
65
|
+
| `iMouseLeftDown` | `float` | `1.0` when left mouse button is pressed, `0.0` otherwise |
|
|
66
|
+
| `iResolution` | `vec2` | Canvas resolution in pixels (includes high-DPI scaling) |
|
|
67
|
+
|
|
68
|
+
## Custom Uniforms
|
|
69
|
+
|
|
70
|
+
Pass custom uniform values via the `uniforms` prop:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<ReactShader
|
|
74
|
+
fragment={fragment}
|
|
75
|
+
uniforms={{
|
|
76
|
+
scale: 2.0, // float
|
|
77
|
+
offset: [0.5, 0.5], // vec2
|
|
78
|
+
color: [1.0, 0.5, 0.2], // vec3
|
|
79
|
+
transform: [1, 0, 0, 1], // vec4
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Supported types:
|
|
85
|
+
- `number` → `float`
|
|
86
|
+
- `[number, number]` → `vec2`
|
|
87
|
+
- `[number, number, number]` → `vec3`
|
|
88
|
+
- `[number, number, number, number]` → `vec4`
|
|
89
|
+
|
|
90
|
+
## Frame Callback
|
|
91
|
+
|
|
92
|
+
Use the `onFrame` callback to update uniforms based on animation timing:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function App() {
|
|
96
|
+
const [customTime, setCustomTime] = useState(0)
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ReactShader
|
|
100
|
+
fragment={fragment}
|
|
101
|
+
uniforms={{ customTime }}
|
|
102
|
+
onFrame={(info) => {
|
|
103
|
+
setCustomTime((prev) => prev + info.deltaTime * 0.5)
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The `FrameInfo` object contains:
|
|
111
|
+
- `deltaTime` - Time since last frame in seconds
|
|
112
|
+
- `time` - Total elapsed time in seconds
|
|
113
|
+
- `resolution` - Canvas resolution as `[width, height]`
|
|
114
|
+
- `mouse` - Mouse position as `[x, y]`
|
|
115
|
+
|
|
116
|
+
## TypeScript
|
|
117
|
+
|
|
118
|
+
All types are exported:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import type {
|
|
122
|
+
Vec2,
|
|
123
|
+
Vec3,
|
|
124
|
+
Vec4,
|
|
125
|
+
UniformValue,
|
|
126
|
+
DefaultUniforms,
|
|
127
|
+
FrameInfo,
|
|
128
|
+
ReactShaderProps,
|
|
129
|
+
} from "@fjandin/react-shader"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Features
|
|
133
|
+
|
|
134
|
+
- WebGL2 with WebGL1 fallback
|
|
135
|
+
- High-DPI display support via `devicePixelRatio`
|
|
136
|
+
- Automatic canvas resizing
|
|
137
|
+
- Shader compilation error display
|
|
138
|
+
- Context loss/restoration handling
|
|
139
|
+
- Mouse tracking with WebGL coordinate convention
|
|
140
|
+
|
|
141
|
+
## Requirements
|
|
142
|
+
|
|
143
|
+
- React >= 17.0.0
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
package/dist/ReactShader.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ReactShaderProps } from "./types";
|
|
2
|
-
export declare function ReactShader({ className, fragment, vertex, uniforms,
|
|
2
|
+
export declare function ReactShader({ className, fragment, vertex, uniforms, fullscreen, timeScale, onFrame, }: ReactShaderProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=ReactShader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactShader.d.ts","sourceRoot":"","sources":["../src/ReactShader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"ReactShader.d.ts","sourceRoot":"","sources":["../src/ReactShader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAU/C,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,QAAQ,EACR,MAAuB,EACvB,QAAQ,EACR,UAAkB,EAClB,SAAa,EACb,OAAO,GACR,EAAE,gBAAgB,2CAwElB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontend.d.ts","sourceRoot":"","sources":["../../src/example/frontend.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,aAAa,CAAA;AAsBpB,wBAAgB,GAAG,
|
|
1
|
+
{"version":3,"file":"frontend.d.ts","sourceRoot":"","sources":["../../src/example/frontend.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,aAAa,CAAA;AAsBpB,wBAAgB,GAAG,4CA6BlB"}
|
package/dist/index.cjs
CHANGED
|
@@ -329,17 +329,11 @@ function ReactShader({
|
|
|
329
329
|
fragment,
|
|
330
330
|
vertex = DEFAULT_VERTEX,
|
|
331
331
|
uniforms,
|
|
332
|
-
debug = false,
|
|
333
332
|
fullscreen = false,
|
|
334
333
|
timeScale = 1,
|
|
335
334
|
onFrame
|
|
336
335
|
}) {
|
|
337
336
|
const [error, setError] = import_react2.useState(null);
|
|
338
|
-
const [debugInfo, setDebugInfo] = import_react2.useState({
|
|
339
|
-
iResolution: [0, 0],
|
|
340
|
-
iMouse: [0, 0]
|
|
341
|
-
});
|
|
342
|
-
const lastDebugUpdateRef = import_react2.useRef(0);
|
|
343
337
|
const handleError = import_react2.useCallback((err) => {
|
|
344
338
|
setError(err.message);
|
|
345
339
|
console.error("ReactShader error:", err);
|
|
@@ -348,17 +342,7 @@ function ReactShader({
|
|
|
348
342
|
if (onFrame) {
|
|
349
343
|
onFrame(info);
|
|
350
344
|
}
|
|
351
|
-
|
|
352
|
-
return;
|
|
353
|
-
const now = performance.now();
|
|
354
|
-
if (now - lastDebugUpdateRef.current < 100)
|
|
355
|
-
return;
|
|
356
|
-
lastDebugUpdateRef.current = now;
|
|
357
|
-
setDebugInfo({
|
|
358
|
-
iResolution: info.resolution,
|
|
359
|
-
iMouse: info.mouse
|
|
360
|
-
});
|
|
361
|
-
}, [debug, onFrame]);
|
|
345
|
+
}, [onFrame]);
|
|
362
346
|
import_react2.useEffect(() => {
|
|
363
347
|
setError(null);
|
|
364
348
|
}, [fragment, vertex]);
|
|
@@ -396,7 +380,9 @@ function ReactShader({
|
|
|
396
380
|
fontSize: "12px",
|
|
397
381
|
padding: "16px",
|
|
398
382
|
overflow: "auto",
|
|
399
|
-
boxSizing: "border-box"
|
|
383
|
+
boxSizing: "border-box",
|
|
384
|
+
width: "100%",
|
|
385
|
+
height: "100%"
|
|
400
386
|
},
|
|
401
387
|
children: /* @__PURE__ */ jsx_dev_runtime.jsxDEV("pre", {
|
|
402
388
|
style: { margin: 0, whiteSpace: "pre-wrap" },
|
|
@@ -404,48 +390,9 @@ function ReactShader({
|
|
|
404
390
|
}, undefined, false, undefined, this)
|
|
405
391
|
}, undefined, false, undefined, this);
|
|
406
392
|
}
|
|
407
|
-
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
className,
|
|
413
|
-
style: { display: "block", width: "100%", height: "100%" }
|
|
414
|
-
}, undefined, false, undefined, this),
|
|
415
|
-
debug && /* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
|
|
416
|
-
style: {
|
|
417
|
-
position: "absolute",
|
|
418
|
-
top: 8,
|
|
419
|
-
left: 8,
|
|
420
|
-
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
421
|
-
color: "#fff",
|
|
422
|
-
fontFamily: "monospace",
|
|
423
|
-
fontSize: "12px",
|
|
424
|
-
padding: "8px",
|
|
425
|
-
borderRadius: "4px",
|
|
426
|
-
pointerEvents: "none"
|
|
427
|
-
},
|
|
428
|
-
children: [
|
|
429
|
-
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
|
|
430
|
-
children: [
|
|
431
|
-
"iResolution: [",
|
|
432
|
-
debugInfo.iResolution[0],
|
|
433
|
-
", ",
|
|
434
|
-
debugInfo.iResolution[1],
|
|
435
|
-
"]"
|
|
436
|
-
]
|
|
437
|
-
}, undefined, true, undefined, this),
|
|
438
|
-
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
|
|
439
|
-
children: [
|
|
440
|
-
"iMouse: [",
|
|
441
|
-
Math.round(debugInfo.iMouse[0]),
|
|
442
|
-
", ",
|
|
443
|
-
Math.round(debugInfo.iMouse[1]),
|
|
444
|
-
"]"
|
|
445
|
-
]
|
|
446
|
-
}, undefined, true, undefined, this)
|
|
447
|
-
]
|
|
448
|
-
}, undefined, true, undefined, this)
|
|
449
|
-
]
|
|
450
|
-
}, undefined, true, undefined, this);
|
|
393
|
+
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("canvas", {
|
|
394
|
+
ref: canvasRef,
|
|
395
|
+
className,
|
|
396
|
+
style: { display: "block", width: "100%", height: "100%" }
|
|
397
|
+
}, undefined, false, undefined, this);
|
|
451
398
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/ReactShader.tsx
|
|
2
|
-
import { useCallback as useCallback2, useEffect as useEffect2,
|
|
2
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useState } from "react";
|
|
3
3
|
|
|
4
4
|
// src/hooks/useWebGL.ts
|
|
5
5
|
import { useCallback, useEffect, useRef } from "react";
|
|
@@ -294,17 +294,11 @@ function ReactShader({
|
|
|
294
294
|
fragment,
|
|
295
295
|
vertex = DEFAULT_VERTEX,
|
|
296
296
|
uniforms,
|
|
297
|
-
debug = false,
|
|
298
297
|
fullscreen = false,
|
|
299
298
|
timeScale = 1,
|
|
300
299
|
onFrame
|
|
301
300
|
}) {
|
|
302
301
|
const [error, setError] = useState(null);
|
|
303
|
-
const [debugInfo, setDebugInfo] = useState({
|
|
304
|
-
iResolution: [0, 0],
|
|
305
|
-
iMouse: [0, 0]
|
|
306
|
-
});
|
|
307
|
-
const lastDebugUpdateRef = useRef2(0);
|
|
308
302
|
const handleError = useCallback2((err) => {
|
|
309
303
|
setError(err.message);
|
|
310
304
|
console.error("ReactShader error:", err);
|
|
@@ -313,17 +307,7 @@ function ReactShader({
|
|
|
313
307
|
if (onFrame) {
|
|
314
308
|
onFrame(info);
|
|
315
309
|
}
|
|
316
|
-
|
|
317
|
-
return;
|
|
318
|
-
const now = performance.now();
|
|
319
|
-
if (now - lastDebugUpdateRef.current < 100)
|
|
320
|
-
return;
|
|
321
|
-
lastDebugUpdateRef.current = now;
|
|
322
|
-
setDebugInfo({
|
|
323
|
-
iResolution: info.resolution,
|
|
324
|
-
iMouse: info.mouse
|
|
325
|
-
});
|
|
326
|
-
}, [debug, onFrame]);
|
|
310
|
+
}, [onFrame]);
|
|
327
311
|
useEffect2(() => {
|
|
328
312
|
setError(null);
|
|
329
313
|
}, [fragment, vertex]);
|
|
@@ -361,7 +345,9 @@ function ReactShader({
|
|
|
361
345
|
fontSize: "12px",
|
|
362
346
|
padding: "16px",
|
|
363
347
|
overflow: "auto",
|
|
364
|
-
boxSizing: "border-box"
|
|
348
|
+
boxSizing: "border-box",
|
|
349
|
+
width: "100%",
|
|
350
|
+
height: "100%"
|
|
365
351
|
},
|
|
366
352
|
children: /* @__PURE__ */ jsxDEV("pre", {
|
|
367
353
|
style: { margin: 0, whiteSpace: "pre-wrap" },
|
|
@@ -369,50 +355,11 @@ function ReactShader({
|
|
|
369
355
|
}, undefined, false, undefined, this)
|
|
370
356
|
}, undefined, false, undefined, this);
|
|
371
357
|
}
|
|
372
|
-
return /* @__PURE__ */ jsxDEV("
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
className,
|
|
378
|
-
style: { display: "block", width: "100%", height: "100%" }
|
|
379
|
-
}, undefined, false, undefined, this),
|
|
380
|
-
debug && /* @__PURE__ */ jsxDEV("div", {
|
|
381
|
-
style: {
|
|
382
|
-
position: "absolute",
|
|
383
|
-
top: 8,
|
|
384
|
-
left: 8,
|
|
385
|
-
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
386
|
-
color: "#fff",
|
|
387
|
-
fontFamily: "monospace",
|
|
388
|
-
fontSize: "12px",
|
|
389
|
-
padding: "8px",
|
|
390
|
-
borderRadius: "4px",
|
|
391
|
-
pointerEvents: "none"
|
|
392
|
-
},
|
|
393
|
-
children: [
|
|
394
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
395
|
-
children: [
|
|
396
|
-
"iResolution: [",
|
|
397
|
-
debugInfo.iResolution[0],
|
|
398
|
-
", ",
|
|
399
|
-
debugInfo.iResolution[1],
|
|
400
|
-
"]"
|
|
401
|
-
]
|
|
402
|
-
}, undefined, true, undefined, this),
|
|
403
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
404
|
-
children: [
|
|
405
|
-
"iMouse: [",
|
|
406
|
-
Math.round(debugInfo.iMouse[0]),
|
|
407
|
-
", ",
|
|
408
|
-
Math.round(debugInfo.iMouse[1]),
|
|
409
|
-
"]"
|
|
410
|
-
]
|
|
411
|
-
}, undefined, true, undefined, this)
|
|
412
|
-
]
|
|
413
|
-
}, undefined, true, undefined, this)
|
|
414
|
-
]
|
|
415
|
-
}, undefined, true, undefined, this);
|
|
358
|
+
return /* @__PURE__ */ jsxDEV("canvas", {
|
|
359
|
+
ref: canvasRef,
|
|
360
|
+
className,
|
|
361
|
+
style: { display: "block", width: "100%", height: "100%" }
|
|
362
|
+
}, undefined, false, undefined, this);
|
|
416
363
|
}
|
|
417
364
|
export {
|
|
418
365
|
ReactShader
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AACnC,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAC3C,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAEnD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACvC,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AACnC,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAC3C,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAEnD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACvC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,IAAI,CAAA;IACZ,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,IAAI,CAAA;CAClB"}
|