react-shadertoy 0.5.0 → 0.6.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/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  Run [Shadertoy](https://www.shadertoy.com/) GLSL shaders in React. Copy-paste and it works.
4
4
 
5
5
  - Zero dependencies (just React)
6
- - All Shadertoy uniforms supported (`iTime`, `iResolution`, `iMouse`, `iDate`, etc.)
7
- - iChannel0-3 textures (image URL, video, canvas)
8
- - Multipass rendering (Buffer A-D Image)
6
+ - WebGL2 (GLSL ES 3.0) full Shadertoy compatibility
7
+ - All uniforms: `iTime`, `iResolution`, `iMouse`, `iDate`, `iFrame`, etc.
8
+ - iChannel0-3 textures (image URL, video, canvas, with wrap/filter/vflip)
9
+ - Multipass rendering (Buffer A-D with ping-pong FBO)
10
+ - Shadertoy API integration (`<Shadertoy id="MdX3zr" />`)
9
11
  - Mouse & touch interaction built-in
10
12
  - TypeScript-first
11
13
 
@@ -15,7 +17,7 @@ Run [Shadertoy](https://www.shadertoy.com/) GLSL shaders in React. Copy-paste an
15
17
  npm install react-shadertoy
16
18
  ```
17
19
 
18
- ## Usage
20
+ ## Quick Start
19
21
 
20
22
  ```tsx
21
23
  import { Shadertoy } from 'react-shadertoy'
@@ -37,13 +39,99 @@ function App() {
37
39
 
38
40
  Find a shader on [Shadertoy](https://www.shadertoy.com/), copy the GLSL code, paste it into `fragmentShader`. Done.
39
41
 
42
+ ## Textures
43
+
44
+ Pass image URLs, video elements, or canvas elements as textures:
45
+
46
+ ```tsx
47
+ <Shadertoy
48
+ fragmentShader={code}
49
+ textures={{
50
+ iChannel0: '/noise.png',
51
+ iChannel1: videoRef.current,
52
+ iChannel2: canvasRef.current,
53
+ }}
54
+ />
55
+ ```
56
+
57
+ ### Texture Options
58
+
59
+ Control wrap mode, filtering, and vertical flip:
60
+
61
+ ```tsx
62
+ <Shadertoy
63
+ fragmentShader={code}
64
+ textures={{
65
+ iChannel0: {
66
+ src: '/noise.png',
67
+ wrap: 'repeat', // 'clamp' | 'repeat' (default: 'clamp')
68
+ filter: 'mipmap', // 'nearest' | 'linear' | 'mipmap' (default: 'mipmap')
69
+ vflip: true, // vertical flip (default: true)
70
+ },
71
+ iChannel1: '/simple.png', // shorthand = default options
72
+ }}
73
+ />
74
+ ```
75
+
76
+ ## Multipass
77
+
78
+ Buffer A-D with self-referencing feedback loops:
79
+
80
+ ```tsx
81
+ <Shadertoy
82
+ passes={{
83
+ BufferA: {
84
+ code: bufferACode,
85
+ iChannel0: 'BufferA', // self-reference (previous frame)
86
+ iChannel1: '/noise.png', // external texture
87
+ },
88
+ BufferB: {
89
+ code: bufferBCode,
90
+ iChannel0: 'BufferA', // read Buffer A output
91
+ },
92
+ Image: {
93
+ code: imageCode,
94
+ iChannel0: 'BufferA',
95
+ iChannel1: 'BufferB',
96
+ },
97
+ }}
98
+ />
99
+ ```
100
+
101
+ ## Shadertoy API
102
+
103
+ Load shaders directly from Shadertoy by ID:
104
+
105
+ ```tsx
106
+ <Shadertoy
107
+ id="MdX3zr"
108
+ apiKey="your-api-key"
109
+ />
110
+ ```
111
+
112
+ Shows an author/name overlay by default. Disable with `showLicense={false}`.
113
+
114
+ API key from [shadertoy.com/myapps](https://www.shadertoy.com/myapps).
115
+
116
+ ### Build-Time Fetch
117
+
118
+ For production, fetch at build time to avoid runtime API calls:
119
+
120
+ ```ts
121
+ import { fetchShader, apiToConfig } from 'react-shadertoy'
122
+
123
+ const shader = await fetchShader('MdX3zr', process.env.SHADERTOY_API_KEY)
124
+ const config = apiToConfig(shader)
125
+ // Save config to JSON, use passes prop at runtime
126
+ ```
127
+
40
128
  ## Hooks API
41
129
 
42
130
  ```tsx
43
131
  import { useShadertoy } from 'react-shadertoy'
44
132
 
45
133
  function MyShader() {
46
- const { canvasRef, isReady, error, pause, resume } = useShadertoy({
134
+ const { canvasRef, isReady, error, pause, resume, meta } = useShadertoy({
47
135
  fragmentShader: `
48
136
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
49
137
  vec2 uv = fragCoord / iResolution.xy;
@@ -60,7 +148,12 @@ function MyShader() {
60
148
 
61
149
  | Prop | Type | Default | Description |
62
150
  |------|------|---------|-------------|
63
- | `fragmentShader` | `string` | required | Shadertoy GLSL code |
151
+ | `fragmentShader` | `string` | | Shadertoy GLSL code |
152
+ | `textures` | `TextureInputs` | — | iChannel0-3 texture sources |
153
+ | `passes` | `MultipassConfig` | — | Multipass Buffer A-D + Image |
154
+ | `id` | `string` | — | Shadertoy shader ID (API mode) |
155
+ | `apiKey` | `string` | — | Shadertoy API key |
156
+ | `showLicense` | `boolean` | `true` (API) | Show author overlay |
64
157
  | `style` | `CSSProperties` | — | Container style |
65
158
  | `className` | `string` | — | Container className |
66
159
  | `paused` | `boolean` | `false` | Pause rendering |
@@ -80,6 +173,12 @@ function MyShader() {
80
173
  | `iFrame` | `int` | Frame counter |
81
174
  | `iMouse` | `vec4` | Mouse position & click state |
82
175
  | `iDate` | `vec4` | Year, month, day, seconds |
176
+ | `iChannel0-3` | `sampler2D` | Texture inputs |
177
+ | `iChannelResolution` | `vec3[4]` | Texture dimensions |
178
+
179
+ ## Why Raw GLSL?
180
+
181
+ AI coding assistants generate raw GLSL far more reliably than framework-specific shader APIs. GLSL is a standard with massive training data — no abstraction layers to get wrong. Ask any AI to "write a Shadertoy shader that does X" and paste the result directly into `fragmentShader`. It just works.
83
182
 
84
183
  ## License
85
184
 
package/dist/index.d.mts CHANGED
@@ -36,6 +36,58 @@ interface PassConfig {
36
36
  type MultipassConfig = {
37
37
  [K in PassName]?: PassConfig;
38
38
  };
39
+ interface ShadertoyApiSampler {
40
+ filter: string;
41
+ wrap: string;
42
+ vflip: string;
43
+ srgb: string;
44
+ internal: string;
45
+ }
46
+ interface ShadertoyApiInput {
47
+ id: number;
48
+ src: string;
49
+ ctype: string;
50
+ channel: number;
51
+ sampler: ShadertoyApiSampler;
52
+ published: number;
53
+ }
54
+ interface ShadertoyApiOutput {
55
+ id: number;
56
+ channel: number;
57
+ }
58
+ interface ShadertoyApiRenderPass {
59
+ inputs: ShadertoyApiInput[];
60
+ outputs: ShadertoyApiOutput[];
61
+ code: string;
62
+ name: string;
63
+ description: string;
64
+ type: string;
65
+ }
66
+ interface ShadertoyApiInfo {
67
+ id: string;
68
+ date: string;
69
+ viewed: number;
70
+ name: string;
71
+ username: string;
72
+ description: string;
73
+ likes: number;
74
+ published: number;
75
+ flags: number;
76
+ tags: string[];
77
+ hasliked: number;
78
+ }
79
+ interface ShadertoyApiShader {
80
+ ver: string;
81
+ info: ShadertoyApiInfo;
82
+ renderpass: ShadertoyApiRenderPass[];
83
+ }
84
+ interface ShaderMeta {
85
+ name: string;
86
+ author: string;
87
+ description: string;
88
+ tags: string[];
89
+ license?: string;
90
+ }
39
91
  interface ShadertoyProps {
40
92
  /** Shadertoy-compatible GLSL fragment shader (must contain mainImage) */
41
93
  fragmentShader?: string;
@@ -43,6 +95,12 @@ interface ShadertoyProps {
43
95
  passes?: MultipassConfig;
44
96
  /** Texture inputs for iChannel0-3 (single-pass mode) */
45
97
  textures?: TextureInputs;
98
+ /** Shadertoy shader ID — fetches shader from API */
99
+ id?: string;
100
+ /** Shadertoy API key (required when using id) */
101
+ apiKey?: string;
102
+ /** Show license/author overlay (default: true when using id) */
103
+ showLicense?: boolean;
46
104
  /** Container style */
47
105
  style?: CSSProperties;
48
106
  /** Container className */
@@ -64,6 +122,8 @@ interface UseShadertoyOptions {
64
122
  fragmentShader?: string;
65
123
  passes?: MultipassConfig;
66
124
  textures?: TextureInputs;
125
+ id?: string;
126
+ apiKey?: string;
67
127
  paused?: boolean;
68
128
  speed?: number;
69
129
  pixelRatio?: number;
@@ -77,10 +137,26 @@ interface UseShadertoyReturn {
77
137
  error: string | null;
78
138
  pause: () => void;
79
139
  resume: () => void;
140
+ meta: ShaderMeta | null;
80
141
  }
81
142
 
82
- declare function Shadertoy({ fragmentShader, passes, textures, style, className, paused, speed, pixelRatio, mouse, onError, onLoad, }: ShadertoyProps): react_jsx_runtime.JSX.Element;
143
+ declare function Shadertoy({ fragmentShader, passes, textures, id, apiKey, showLicense, style, className, paused, speed, pixelRatio, mouse, onError, onLoad, }: ShadertoyProps): react_jsx_runtime.JSX.Element;
83
144
 
84
- declare function useShadertoy({ fragmentShader, passes: passesProp, textures: texturesProp, paused, speed, pixelRatio, mouse: mouseEnabled, onError, onLoad, }: UseShadertoyOptions): UseShadertoyReturn;
145
+ declare function useShadertoy({ fragmentShader, passes: passesProp, textures: texturesProp, id, apiKey, paused, speed, pixelRatio, mouse: mouseEnabled, onError, onLoad, }: UseShadertoyOptions): UseShadertoyReturn & {
146
+ meta: ShaderMeta | null;
147
+ };
148
+
149
+ /**
150
+ * Fetch a shader from the Shadertoy API.
151
+ */
152
+ declare function fetchShader(id: string, apiKey: string): Promise<ShadertoyApiShader>;
153
+ /**
154
+ * Convert a Shadertoy API shader to our MultipassConfig + TextureInputs + meta.
155
+ */
156
+ declare function apiToConfig(shader: ShadertoyApiShader): {
157
+ passes: MultipassConfig;
158
+ textures: TextureInputs;
159
+ meta: ShaderMeta;
160
+ };
85
161
 
86
- export { type MultipassConfig, type PassConfig, type PassName, Shadertoy, type ShadertoyProps, type TextureFilter, type TextureInput, type TextureInputs, type TextureOptions, type TextureWrap, type UseShadertoyOptions, type UseShadertoyReturn, useShadertoy };
162
+ export { type MultipassConfig, type PassConfig, type PassName, type ShaderMeta, Shadertoy, type ShadertoyApiShader, type ShadertoyProps, type TextureFilter, type TextureInput, type TextureInputs, type TextureOptions, type TextureWrap, type UseShadertoyOptions, type UseShadertoyReturn, apiToConfig, fetchShader, useShadertoy };
package/dist/index.d.ts CHANGED
@@ -36,6 +36,58 @@ interface PassConfig {
36
36
  type MultipassConfig = {
37
37
  [K in PassName]?: PassConfig;
38
38
  };
39
+ interface ShadertoyApiSampler {
40
+ filter: string;
41
+ wrap: string;
42
+ vflip: string;
43
+ srgb: string;
44
+ internal: string;
45
+ }
46
+ interface ShadertoyApiInput {
47
+ id: number;
48
+ src: string;
49
+ ctype: string;
50
+ channel: number;
51
+ sampler: ShadertoyApiSampler;
52
+ published: number;
53
+ }
54
+ interface ShadertoyApiOutput {
55
+ id: number;
56
+ channel: number;
57
+ }
58
+ interface ShadertoyApiRenderPass {
59
+ inputs: ShadertoyApiInput[];
60
+ outputs: ShadertoyApiOutput[];
61
+ code: string;
62
+ name: string;
63
+ description: string;
64
+ type: string;
65
+ }
66
+ interface ShadertoyApiInfo {
67
+ id: string;
68
+ date: string;
69
+ viewed: number;
70
+ name: string;
71
+ username: string;
72
+ description: string;
73
+ likes: number;
74
+ published: number;
75
+ flags: number;
76
+ tags: string[];
77
+ hasliked: number;
78
+ }
79
+ interface ShadertoyApiShader {
80
+ ver: string;
81
+ info: ShadertoyApiInfo;
82
+ renderpass: ShadertoyApiRenderPass[];
83
+ }
84
+ interface ShaderMeta {
85
+ name: string;
86
+ author: string;
87
+ description: string;
88
+ tags: string[];
89
+ license?: string;
90
+ }
39
91
  interface ShadertoyProps {
40
92
  /** Shadertoy-compatible GLSL fragment shader (must contain mainImage) */
41
93
  fragmentShader?: string;
@@ -43,6 +95,12 @@ interface ShadertoyProps {
43
95
  passes?: MultipassConfig;
44
96
  /** Texture inputs for iChannel0-3 (single-pass mode) */
45
97
  textures?: TextureInputs;
98
+ /** Shadertoy shader ID — fetches shader from API */
99
+ id?: string;
100
+ /** Shadertoy API key (required when using id) */
101
+ apiKey?: string;
102
+ /** Show license/author overlay (default: true when using id) */
103
+ showLicense?: boolean;
46
104
  /** Container style */
47
105
  style?: CSSProperties;
48
106
  /** Container className */
@@ -64,6 +122,8 @@ interface UseShadertoyOptions {
64
122
  fragmentShader?: string;
65
123
  passes?: MultipassConfig;
66
124
  textures?: TextureInputs;
125
+ id?: string;
126
+ apiKey?: string;
67
127
  paused?: boolean;
68
128
  speed?: number;
69
129
  pixelRatio?: number;
@@ -77,10 +137,26 @@ interface UseShadertoyReturn {
77
137
  error: string | null;
78
138
  pause: () => void;
79
139
  resume: () => void;
140
+ meta: ShaderMeta | null;
80
141
  }
81
142
 
82
- declare function Shadertoy({ fragmentShader, passes, textures, style, className, paused, speed, pixelRatio, mouse, onError, onLoad, }: ShadertoyProps): react_jsx_runtime.JSX.Element;
143
+ declare function Shadertoy({ fragmentShader, passes, textures, id, apiKey, showLicense, style, className, paused, speed, pixelRatio, mouse, onError, onLoad, }: ShadertoyProps): react_jsx_runtime.JSX.Element;
83
144
 
84
- declare function useShadertoy({ fragmentShader, passes: passesProp, textures: texturesProp, paused, speed, pixelRatio, mouse: mouseEnabled, onError, onLoad, }: UseShadertoyOptions): UseShadertoyReturn;
145
+ declare function useShadertoy({ fragmentShader, passes: passesProp, textures: texturesProp, id, apiKey, paused, speed, pixelRatio, mouse: mouseEnabled, onError, onLoad, }: UseShadertoyOptions): UseShadertoyReturn & {
146
+ meta: ShaderMeta | null;
147
+ };
148
+
149
+ /**
150
+ * Fetch a shader from the Shadertoy API.
151
+ */
152
+ declare function fetchShader(id: string, apiKey: string): Promise<ShadertoyApiShader>;
153
+ /**
154
+ * Convert a Shadertoy API shader to our MultipassConfig + TextureInputs + meta.
155
+ */
156
+ declare function apiToConfig(shader: ShadertoyApiShader): {
157
+ passes: MultipassConfig;
158
+ textures: TextureInputs;
159
+ meta: ShaderMeta;
160
+ };
85
161
 
86
- export { type MultipassConfig, type PassConfig, type PassName, Shadertoy, type ShadertoyProps, type TextureFilter, type TextureInput, type TextureInputs, type TextureOptions, type TextureWrap, type UseShadertoyOptions, type UseShadertoyReturn, useShadertoy };
162
+ export { type MultipassConfig, type PassConfig, type PassName, type ShaderMeta, Shadertoy, type ShadertoyApiShader, type ShadertoyProps, type TextureFilter, type TextureInput, type TextureInputs, type TextureOptions, type TextureWrap, type UseShadertoyOptions, type UseShadertoyReturn, apiToConfig, fetchShader, useShadertoy };
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Shadertoy: () => Shadertoy,
24
+ apiToConfig: () => apiToConfig,
25
+ fetchShader: () => fetchShader,
24
26
  useShadertoy: () => useShadertoy
25
27
  });
26
28
  module.exports = __toCommonJS(index_exports);
@@ -28,6 +30,98 @@ module.exports = __toCommonJS(index_exports);
28
30
  // src/useShadertoy.ts
29
31
  var import_react = require("react");
30
32
 
33
+ // src/api.ts
34
+ var SHADERTOY_BASE = "https://www.shadertoy.com";
35
+ var API_URL = `${SHADERTOY_BASE}/api/v1/shaders`;
36
+ var OUTPUT_ID_TO_PASS = {
37
+ 257: "BufferA",
38
+ 258: "BufferB",
39
+ 259: "BufferC",
40
+ 260: "BufferD"
41
+ };
42
+ var cache = /* @__PURE__ */ new Map();
43
+ async function fetchShader(id, apiKey) {
44
+ const cached = cache.get(id);
45
+ if (cached) return cached;
46
+ const res = await fetch(`${API_URL}/${id}?key=${apiKey}`);
47
+ if (!res.ok) throw new Error(`Shadertoy API error: ${res.status}`);
48
+ const data = await res.json();
49
+ if (data.Error) throw new Error(`Shadertoy API: ${data.Error}`);
50
+ cache.set(id, data.Shader);
51
+ return data.Shader;
52
+ }
53
+ function mapWrap(wrap) {
54
+ if (wrap === "repeat") return "repeat";
55
+ return "clamp";
56
+ }
57
+ function mapFilter(filter) {
58
+ if (filter === "nearest") return "nearest";
59
+ if (filter === "linear") return "linear";
60
+ return "mipmap";
61
+ }
62
+ function resolveTextureSrc(src) {
63
+ if (src.startsWith("http")) return src;
64
+ return SHADERTOY_BASE + src;
65
+ }
66
+ function apiToConfig(shader) {
67
+ const passes = {};
68
+ const textures = {};
69
+ const commonPass = shader.renderpass.find((p) => p.type === "common");
70
+ const commonCode = commonPass ? commonPass.code + "\n" : "";
71
+ for (const rp of shader.renderpass) {
72
+ if (rp.type === "common" || rp.type === "sound") continue;
73
+ const passName = getPassName(rp);
74
+ if (!passName) continue;
75
+ const passConfig = {
76
+ code: commonCode + rp.code
77
+ };
78
+ for (const input of rp.inputs) {
79
+ const channelKey = `iChannel${input.channel}`;
80
+ if (channelKey === "code") continue;
81
+ if (input.ctype === "buffer") {
82
+ const refPass = OUTPUT_ID_TO_PASS[input.id];
83
+ if (refPass) {
84
+ ;
85
+ passConfig[channelKey] = refPass;
86
+ }
87
+ } else if (input.ctype === "texture" || input.ctype === "cubemap") {
88
+ const texOpts = {
89
+ src: resolveTextureSrc(input.src),
90
+ wrap: mapWrap(input.sampler.wrap),
91
+ filter: mapFilter(input.sampler.filter),
92
+ vflip: input.sampler.vflip === "true"
93
+ };
94
+ passConfig[channelKey] = texOpts;
95
+ const texKey = `iChannel${input.channel}`;
96
+ textures[texKey] = texOpts;
97
+ }
98
+ }
99
+ passes[passName] = passConfig;
100
+ }
101
+ const meta = {
102
+ name: shader.info.name,
103
+ author: shader.info.username,
104
+ description: shader.info.description,
105
+ tags: shader.info.tags
106
+ };
107
+ return { passes, textures, meta };
108
+ }
109
+ function getPassName(rp) {
110
+ if (rp.type === "image") return "Image";
111
+ if (rp.type === "buffer") {
112
+ for (const out of rp.outputs) {
113
+ const name = OUTPUT_ID_TO_PASS[out.id];
114
+ if (name) return name;
115
+ }
116
+ return null;
117
+ }
118
+ return null;
119
+ }
120
+ function isSinglePass(passes) {
121
+ const keys = Object.keys(passes);
122
+ return keys.length === 1 && keys[0] === "Image";
123
+ }
124
+
31
125
  // src/renderer.ts
32
126
  var QUAD_VERTICES = new Float32Array([
33
127
  -1,
@@ -578,6 +672,8 @@ function useShadertoy({
578
672
  fragmentShader,
579
673
  passes: passesProp,
580
674
  textures: texturesProp,
675
+ id,
676
+ apiKey,
581
677
  paused = false,
582
678
  speed = 1,
583
679
  pixelRatio,
@@ -593,6 +689,8 @@ function useShadertoy({
593
689
  const speedRef = (0, import_react.useRef)(speed);
594
690
  const [isReady, setIsReady] = (0, import_react.useState)(false);
595
691
  const [error, setError] = (0, import_react.useState)(null);
692
+ const [meta, setMeta] = (0, import_react.useState)(null);
693
+ const [resolved, setResolved] = (0, import_react.useState)(id ? null : { passes: passesProp, textures: texturesProp, fragmentShader });
596
694
  const mouseState = (0, import_react.useRef)({
597
695
  x: 0,
598
696
  y: 0,
@@ -603,8 +701,43 @@ function useShadertoy({
603
701
  const sharedState = (0, import_react.useRef)({ time: 0, frame: 0 });
604
702
  pausedRef.current = paused;
605
703
  speedRef.current = speed;
606
- const isMultipass = !!passesProp;
607
704
  (0, import_react.useEffect)(() => {
705
+ if (!id) return;
706
+ if (!apiKey) {
707
+ setError("apiKey is required when using id");
708
+ onError?.("apiKey is required when using id");
709
+ return;
710
+ }
711
+ let cancelled = false;
712
+ fetchShader(id, apiKey).then((shader) => {
713
+ if (cancelled) return;
714
+ const config = apiToConfig(shader);
715
+ setMeta(config.meta);
716
+ if (isSinglePass(config.passes)) {
717
+ const imagePass = config.passes.Image;
718
+ setResolved({
719
+ fragmentShader: imagePass.code,
720
+ textures: config.textures
721
+ });
722
+ } else {
723
+ setResolved({ passes: config.passes });
724
+ }
725
+ }).catch((err) => {
726
+ if (cancelled) return;
727
+ const msg = err instanceof Error ? err.message : "Failed to fetch shader";
728
+ setError(msg);
729
+ onError?.(msg);
730
+ });
731
+ return () => {
732
+ cancelled = true;
733
+ };
734
+ }, [id, apiKey]);
735
+ const effectivePasses = resolved?.passes;
736
+ const effectiveTextures = resolved?.textures ?? texturesProp;
737
+ const effectiveShader = resolved?.fragmentShader ?? fragmentShader;
738
+ const isMultipass = !!effectivePasses;
739
+ (0, import_react.useEffect)(() => {
740
+ if (id && !resolved) return;
608
741
  const canvas = canvasRef.current;
609
742
  if (!canvas) return;
610
743
  sharedState.current = { time: 0, frame: 0 };
@@ -621,9 +754,9 @@ function useShadertoy({
621
754
  }
622
755
  const externalTextures = [null, null, null, null];
623
756
  const texturePromises = [];
624
- if (texturesProp) {
757
+ if (effectiveTextures) {
625
758
  for (let i = 0; i < 4; i++) {
626
- const src = texturesProp[CHANNEL_KEYS2[i]];
759
+ const src = effectiveTextures[CHANNEL_KEYS2[i]];
627
760
  if (src != null) {
628
761
  const { state, promise } = createTexture(gl, src, i);
629
762
  externalTextures[i] = state;
@@ -641,7 +774,7 @@ function useShadertoy({
641
774
  onError?.(msg);
642
775
  };
643
776
  if (isMultipass) {
644
- const passResult = createMultipassRenderer(gl, passesProp, externalTextures);
777
+ const passResult = createMultipassRenderer(gl, effectivePasses, externalTextures);
645
778
  if (typeof passResult === "string") {
646
779
  handleError(passResult);
647
780
  return;
@@ -677,7 +810,7 @@ function useShadertoy({
677
810
  setIsReady(false);
678
811
  };
679
812
  } else {
680
- const shaderCode = fragmentShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
813
+ const shaderCode = effectiveShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
681
814
  const result = createRenderer(canvas, shaderCode);
682
815
  if (typeof result === "string") {
683
816
  handleError(result);
@@ -717,7 +850,7 @@ function useShadertoy({
717
850
  setIsReady(false);
718
851
  };
719
852
  }
720
- }, [fragmentShader, passesProp, texturesProp, onError, onLoad]);
853
+ }, [effectiveShader, effectivePasses, effectiveTextures, resolved, onError, onLoad]);
721
854
  (0, import_react.useEffect)(() => {
722
855
  const canvas = canvasRef.current;
723
856
  if (!canvas) return;
@@ -799,7 +932,7 @@ function useShadertoy({
799
932
  const resume = (0, import_react.useCallback)(() => {
800
933
  pausedRef.current = false;
801
934
  }, []);
802
- return { canvasRef, isReady, error, pause, resume };
935
+ return { canvasRef, isReady, error, pause, resume, meta };
803
936
  }
804
937
 
805
938
  // src/Shadertoy.tsx
@@ -808,6 +941,9 @@ function Shadertoy({
808
941
  fragmentShader,
809
942
  passes,
810
943
  textures,
944
+ id,
945
+ apiKey,
946
+ showLicense,
811
947
  style,
812
948
  className,
813
949
  paused,
@@ -817,10 +953,12 @@ function Shadertoy({
817
953
  onError,
818
954
  onLoad
819
955
  }) {
820
- const { canvasRef } = useShadertoy({
956
+ const { canvasRef, meta } = useShadertoy({
821
957
  fragmentShader,
822
958
  passes,
823
959
  textures,
960
+ id,
961
+ apiKey,
824
962
  paused,
825
963
  speed,
826
964
  pixelRatio,
@@ -828,6 +966,35 @@ function Shadertoy({
828
966
  onError,
829
967
  onLoad
830
968
  });
969
+ const shouldShowLicense = showLicense ?? !!id;
970
+ const hasMeta = shouldShowLicense && meta;
971
+ if (hasMeta) {
972
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative", ...style }, className, children: [
973
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
974
+ "canvas",
975
+ {
976
+ ref: canvasRef,
977
+ style: { width: "100%", height: "100%", display: "block" }
978
+ }
979
+ ),
980
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
981
+ position: "absolute",
982
+ bottom: 8,
983
+ right: 8,
984
+ background: "rgba(0,0,0,0.6)",
985
+ color: "#fff",
986
+ padding: "4px 10px",
987
+ borderRadius: 4,
988
+ fontSize: 12,
989
+ fontFamily: "system-ui, sans-serif",
990
+ pointerEvents: "none"
991
+ }, children: [
992
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: meta.name }),
993
+ " by ",
994
+ meta.author
995
+ ] })
996
+ ] });
997
+ }
831
998
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
832
999
  "canvas",
833
1000
  {
@@ -840,5 +1007,7 @@ function Shadertoy({
840
1007
  // Annotate the CommonJS export names for ESM import in node:
841
1008
  0 && (module.exports = {
842
1009
  Shadertoy,
1010
+ apiToConfig,
1011
+ fetchShader,
843
1012
  useShadertoy
844
1013
  });
package/dist/index.mjs CHANGED
@@ -1,6 +1,98 @@
1
1
  // src/useShadertoy.ts
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
 
4
+ // src/api.ts
5
+ var SHADERTOY_BASE = "https://www.shadertoy.com";
6
+ var API_URL = `${SHADERTOY_BASE}/api/v1/shaders`;
7
+ var OUTPUT_ID_TO_PASS = {
8
+ 257: "BufferA",
9
+ 258: "BufferB",
10
+ 259: "BufferC",
11
+ 260: "BufferD"
12
+ };
13
+ var cache = /* @__PURE__ */ new Map();
14
+ async function fetchShader(id, apiKey) {
15
+ const cached = cache.get(id);
16
+ if (cached) return cached;
17
+ const res = await fetch(`${API_URL}/${id}?key=${apiKey}`);
18
+ if (!res.ok) throw new Error(`Shadertoy API error: ${res.status}`);
19
+ const data = await res.json();
20
+ if (data.Error) throw new Error(`Shadertoy API: ${data.Error}`);
21
+ cache.set(id, data.Shader);
22
+ return data.Shader;
23
+ }
24
+ function mapWrap(wrap) {
25
+ if (wrap === "repeat") return "repeat";
26
+ return "clamp";
27
+ }
28
+ function mapFilter(filter) {
29
+ if (filter === "nearest") return "nearest";
30
+ if (filter === "linear") return "linear";
31
+ return "mipmap";
32
+ }
33
+ function resolveTextureSrc(src) {
34
+ if (src.startsWith("http")) return src;
35
+ return SHADERTOY_BASE + src;
36
+ }
37
+ function apiToConfig(shader) {
38
+ const passes = {};
39
+ const textures = {};
40
+ const commonPass = shader.renderpass.find((p) => p.type === "common");
41
+ const commonCode = commonPass ? commonPass.code + "\n" : "";
42
+ for (const rp of shader.renderpass) {
43
+ if (rp.type === "common" || rp.type === "sound") continue;
44
+ const passName = getPassName(rp);
45
+ if (!passName) continue;
46
+ const passConfig = {
47
+ code: commonCode + rp.code
48
+ };
49
+ for (const input of rp.inputs) {
50
+ const channelKey = `iChannel${input.channel}`;
51
+ if (channelKey === "code") continue;
52
+ if (input.ctype === "buffer") {
53
+ const refPass = OUTPUT_ID_TO_PASS[input.id];
54
+ if (refPass) {
55
+ ;
56
+ passConfig[channelKey] = refPass;
57
+ }
58
+ } else if (input.ctype === "texture" || input.ctype === "cubemap") {
59
+ const texOpts = {
60
+ src: resolveTextureSrc(input.src),
61
+ wrap: mapWrap(input.sampler.wrap),
62
+ filter: mapFilter(input.sampler.filter),
63
+ vflip: input.sampler.vflip === "true"
64
+ };
65
+ passConfig[channelKey] = texOpts;
66
+ const texKey = `iChannel${input.channel}`;
67
+ textures[texKey] = texOpts;
68
+ }
69
+ }
70
+ passes[passName] = passConfig;
71
+ }
72
+ const meta = {
73
+ name: shader.info.name,
74
+ author: shader.info.username,
75
+ description: shader.info.description,
76
+ tags: shader.info.tags
77
+ };
78
+ return { passes, textures, meta };
79
+ }
80
+ function getPassName(rp) {
81
+ if (rp.type === "image") return "Image";
82
+ if (rp.type === "buffer") {
83
+ for (const out of rp.outputs) {
84
+ const name = OUTPUT_ID_TO_PASS[out.id];
85
+ if (name) return name;
86
+ }
87
+ return null;
88
+ }
89
+ return null;
90
+ }
91
+ function isSinglePass(passes) {
92
+ const keys = Object.keys(passes);
93
+ return keys.length === 1 && keys[0] === "Image";
94
+ }
95
+
4
96
  // src/renderer.ts
5
97
  var QUAD_VERTICES = new Float32Array([
6
98
  -1,
@@ -551,6 +643,8 @@ function useShadertoy({
551
643
  fragmentShader,
552
644
  passes: passesProp,
553
645
  textures: texturesProp,
646
+ id,
647
+ apiKey,
554
648
  paused = false,
555
649
  speed = 1,
556
650
  pixelRatio,
@@ -566,6 +660,8 @@ function useShadertoy({
566
660
  const speedRef = useRef(speed);
567
661
  const [isReady, setIsReady] = useState(false);
568
662
  const [error, setError] = useState(null);
663
+ const [meta, setMeta] = useState(null);
664
+ const [resolved, setResolved] = useState(id ? null : { passes: passesProp, textures: texturesProp, fragmentShader });
569
665
  const mouseState = useRef({
570
666
  x: 0,
571
667
  y: 0,
@@ -576,8 +672,43 @@ function useShadertoy({
576
672
  const sharedState = useRef({ time: 0, frame: 0 });
577
673
  pausedRef.current = paused;
578
674
  speedRef.current = speed;
579
- const isMultipass = !!passesProp;
580
675
  useEffect(() => {
676
+ if (!id) return;
677
+ if (!apiKey) {
678
+ setError("apiKey is required when using id");
679
+ onError?.("apiKey is required when using id");
680
+ return;
681
+ }
682
+ let cancelled = false;
683
+ fetchShader(id, apiKey).then((shader) => {
684
+ if (cancelled) return;
685
+ const config = apiToConfig(shader);
686
+ setMeta(config.meta);
687
+ if (isSinglePass(config.passes)) {
688
+ const imagePass = config.passes.Image;
689
+ setResolved({
690
+ fragmentShader: imagePass.code,
691
+ textures: config.textures
692
+ });
693
+ } else {
694
+ setResolved({ passes: config.passes });
695
+ }
696
+ }).catch((err) => {
697
+ if (cancelled) return;
698
+ const msg = err instanceof Error ? err.message : "Failed to fetch shader";
699
+ setError(msg);
700
+ onError?.(msg);
701
+ });
702
+ return () => {
703
+ cancelled = true;
704
+ };
705
+ }, [id, apiKey]);
706
+ const effectivePasses = resolved?.passes;
707
+ const effectiveTextures = resolved?.textures ?? texturesProp;
708
+ const effectiveShader = resolved?.fragmentShader ?? fragmentShader;
709
+ const isMultipass = !!effectivePasses;
710
+ useEffect(() => {
711
+ if (id && !resolved) return;
581
712
  const canvas = canvasRef.current;
582
713
  if (!canvas) return;
583
714
  sharedState.current = { time: 0, frame: 0 };
@@ -594,9 +725,9 @@ function useShadertoy({
594
725
  }
595
726
  const externalTextures = [null, null, null, null];
596
727
  const texturePromises = [];
597
- if (texturesProp) {
728
+ if (effectiveTextures) {
598
729
  for (let i = 0; i < 4; i++) {
599
- const src = texturesProp[CHANNEL_KEYS2[i]];
730
+ const src = effectiveTextures[CHANNEL_KEYS2[i]];
600
731
  if (src != null) {
601
732
  const { state, promise } = createTexture(gl, src, i);
602
733
  externalTextures[i] = state;
@@ -614,7 +745,7 @@ function useShadertoy({
614
745
  onError?.(msg);
615
746
  };
616
747
  if (isMultipass) {
617
- const passResult = createMultipassRenderer(gl, passesProp, externalTextures);
748
+ const passResult = createMultipassRenderer(gl, effectivePasses, externalTextures);
618
749
  if (typeof passResult === "string") {
619
750
  handleError(passResult);
620
751
  return;
@@ -650,7 +781,7 @@ function useShadertoy({
650
781
  setIsReady(false);
651
782
  };
652
783
  } else {
653
- const shaderCode = fragmentShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
784
+ const shaderCode = effectiveShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
654
785
  const result = createRenderer(canvas, shaderCode);
655
786
  if (typeof result === "string") {
656
787
  handleError(result);
@@ -690,7 +821,7 @@ function useShadertoy({
690
821
  setIsReady(false);
691
822
  };
692
823
  }
693
- }, [fragmentShader, passesProp, texturesProp, onError, onLoad]);
824
+ }, [effectiveShader, effectivePasses, effectiveTextures, resolved, onError, onLoad]);
694
825
  useEffect(() => {
695
826
  const canvas = canvasRef.current;
696
827
  if (!canvas) return;
@@ -772,15 +903,18 @@ function useShadertoy({
772
903
  const resume = useCallback(() => {
773
904
  pausedRef.current = false;
774
905
  }, []);
775
- return { canvasRef, isReady, error, pause, resume };
906
+ return { canvasRef, isReady, error, pause, resume, meta };
776
907
  }
777
908
 
778
909
  // src/Shadertoy.tsx
779
- import { jsx } from "react/jsx-runtime";
910
+ import { jsx, jsxs } from "react/jsx-runtime";
780
911
  function Shadertoy({
781
912
  fragmentShader,
782
913
  passes,
783
914
  textures,
915
+ id,
916
+ apiKey,
917
+ showLicense,
784
918
  style,
785
919
  className,
786
920
  paused,
@@ -790,10 +924,12 @@ function Shadertoy({
790
924
  onError,
791
925
  onLoad
792
926
  }) {
793
- const { canvasRef } = useShadertoy({
927
+ const { canvasRef, meta } = useShadertoy({
794
928
  fragmentShader,
795
929
  passes,
796
930
  textures,
931
+ id,
932
+ apiKey,
797
933
  paused,
798
934
  speed,
799
935
  pixelRatio,
@@ -801,6 +937,35 @@ function Shadertoy({
801
937
  onError,
802
938
  onLoad
803
939
  });
940
+ const shouldShowLicense = showLicense ?? !!id;
941
+ const hasMeta = shouldShowLicense && meta;
942
+ if (hasMeta) {
943
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", ...style }, className, children: [
944
+ /* @__PURE__ */ jsx(
945
+ "canvas",
946
+ {
947
+ ref: canvasRef,
948
+ style: { width: "100%", height: "100%", display: "block" }
949
+ }
950
+ ),
951
+ /* @__PURE__ */ jsxs("div", { style: {
952
+ position: "absolute",
953
+ bottom: 8,
954
+ right: 8,
955
+ background: "rgba(0,0,0,0.6)",
956
+ color: "#fff",
957
+ padding: "4px 10px",
958
+ borderRadius: 4,
959
+ fontSize: 12,
960
+ fontFamily: "system-ui, sans-serif",
961
+ pointerEvents: "none"
962
+ }, children: [
963
+ /* @__PURE__ */ jsx("strong", { children: meta.name }),
964
+ " by ",
965
+ meta.author
966
+ ] })
967
+ ] });
968
+ }
804
969
  return /* @__PURE__ */ jsx(
805
970
  "canvas",
806
971
  {
@@ -812,5 +977,7 @@ function Shadertoy({
812
977
  }
813
978
  export {
814
979
  Shadertoy,
980
+ apiToConfig,
981
+ fetchShader,
815
982
  useShadertoy
816
983
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-shadertoy",
3
- "version": "0.5.0",
4
- "description": "Run Shadertoy GLSL shaders in React. Copy-paste and it works.",
3
+ "version": "0.6.1",
4
+ "description": "Run GLSL shaders in React — Shadertoy compatible, AI-friendly. Paste raw GLSL and it just works.",
5
5
  "author": "Wrennly (https://github.com/wrennly)",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/wrennly/react-shadertoy",
@@ -14,12 +14,13 @@
14
14
  "shadertoy",
15
15
  "glsl",
16
16
  "webgl",
17
+ "webgl2",
17
18
  "shader",
18
19
  "fragment-shader",
19
20
  "creative-coding",
20
21
  "generative-art",
21
- "webgpu",
22
- "three.js"
22
+ "multipass",
23
+ "ai-friendly"
23
24
  ],
24
25
  "main": "./dist/index.js",
25
26
  "module": "./dist/index.mjs",