react-shadertoy 0.3.0 → 0.6.0

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/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,
@@ -43,14 +137,16 @@ var QUAD_VERTICES = new Float32Array([
43
137
  1,
44
138
  1
45
139
  ]);
46
- var VERTEX_SHADER = `
47
- attribute vec2 position;
140
+ var VERTEX_SHADER = `#version 300 es
141
+ in vec2 position;
48
142
  void main() {
49
143
  gl_Position = vec4(position, 0.0, 1.0);
50
144
  }
51
145
  `;
52
- function wrapFragmentShader(shader) {
53
- return `precision highp float;
146
+ var FRAGMENT_PREAMBLE = `#version 300 es
147
+ precision highp float;
148
+
149
+ out vec4 _fragColor;
54
150
 
55
151
  uniform vec3 iResolution;
56
152
  uniform float iTime;
@@ -64,13 +160,16 @@ uniform sampler2D iChannel2;
64
160
  uniform sampler2D iChannel3;
65
161
  uniform vec3 iChannelResolution[4];
66
162
 
67
- // Shadertoy compatibility: texture() is GLSL 300 es, WebGL1 uses texture2D()
68
- #define texture texture2D
163
+ // Shadertoy compat: older shaders may use texture2D()
164
+ #define texture2D texture
69
165
 
70
- ${shader}
166
+ `;
167
+ function wrapFragmentShader(shader) {
168
+ return FRAGMENT_PREAMBLE + shader + `
71
169
 
72
170
  void main() {
73
- mainImage(gl_FragColor, gl_FragCoord.xy);
171
+ mainImage(_fragColor, gl_FragCoord.xy);
172
+ _fragColor.a = 1.0;
74
173
  }
75
174
  `;
76
175
  }
@@ -86,13 +185,7 @@ function compileShader(gl, type, source) {
86
185
  }
87
186
  return shader;
88
187
  }
89
- function createRenderer(canvas, fragmentShader) {
90
- const gl = canvas.getContext("webgl", {
91
- antialias: false,
92
- alpha: true,
93
- premultipliedAlpha: false
94
- });
95
- if (!gl) return "WebGL not supported";
188
+ function createProgram(gl, fragmentShader) {
96
189
  const vert = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
97
190
  if (typeof vert === "string") return vert;
98
191
  const frag = compileShader(gl, gl.FRAGMENT_SHADER, wrapFragmentShader(fragmentShader));
@@ -109,13 +202,10 @@ function createRenderer(canvas, fragmentShader) {
109
202
  }
110
203
  gl.deleteShader(vert);
111
204
  gl.deleteShader(frag);
112
- const buffer = gl.createBuffer();
113
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
114
- gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
115
- const positionLoc = gl.getAttribLocation(program, "position");
116
- gl.enableVertexAttribArray(positionLoc);
117
- gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
118
- const locations = {
205
+ return program;
206
+ }
207
+ function getUniformLocations(gl, program) {
208
+ return {
119
209
  iResolution: gl.getUniformLocation(program, "iResolution"),
120
210
  iTime: gl.getUniformLocation(program, "iTime"),
121
211
  iTimeDelta: gl.getUniformLocation(program, "iTimeDelta"),
@@ -130,6 +220,26 @@ function createRenderer(canvas, fragmentShader) {
130
220
  ],
131
221
  iChannelResolution: gl.getUniformLocation(program, "iChannelResolution")
132
222
  };
223
+ }
224
+ function setupQuad(gl, program) {
225
+ const buffer = gl.createBuffer();
226
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
227
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
228
+ const positionLoc = gl.getAttribLocation(program, "position");
229
+ gl.enableVertexAttribArray(positionLoc);
230
+ gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
231
+ }
232
+ function createRenderer(canvas, fragmentShader) {
233
+ const gl = canvas.getContext("webgl2", {
234
+ antialias: false,
235
+ alpha: true,
236
+ premultipliedAlpha: false
237
+ });
238
+ if (!gl) return "WebGL2 not supported";
239
+ const program = createProgram(gl, fragmentShader);
240
+ if (typeof program === "string") return program;
241
+ setupQuad(gl, program);
242
+ const locations = getUniformLocations(gl, program);
133
243
  gl.useProgram(program);
134
244
  return {
135
245
  gl,
@@ -153,8 +263,19 @@ function dispose(state) {
153
263
  }
154
264
 
155
265
  // src/textures.ts
156
- function isPOT(v) {
157
- return (v & v - 1) === 0 && v > 0;
266
+ function normalizeTextureInput(input) {
267
+ if (typeof input === "object" && input !== null && "src" in input) {
268
+ return input;
269
+ }
270
+ return { src: input };
271
+ }
272
+ function resolveOptions(opts) {
273
+ return {
274
+ src: opts.src,
275
+ wrap: opts.wrap ?? "clamp",
276
+ filter: opts.filter ?? "mipmap",
277
+ vflip: opts.vflip ?? true
278
+ };
158
279
  }
159
280
  function initTexture(gl, unit) {
160
281
  const texture = gl.createTexture();
@@ -166,12 +287,32 @@ function initTexture(gl, unit) {
166
287
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
167
288
  return texture;
168
289
  }
290
+ function applyTextureParameters(gl, w, h, wrap, filter, vflip) {
291
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, vflip ? 1 : 0);
292
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
293
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
294
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
295
+ if (filter === "mipmap") {
296
+ gl.generateMipmap(gl.TEXTURE_2D);
297
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
298
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
299
+ } else if (filter === "nearest") {
300
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
301
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
302
+ } else {
303
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
304
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
305
+ }
306
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
307
+ }
169
308
  function uploadElement(gl, texture, unit, el) {
170
309
  gl.activeTexture(gl.TEXTURE0 + unit);
171
310
  gl.bindTexture(gl.TEXTURE_2D, texture);
172
311
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
173
312
  }
174
- function createTexture(gl, source, unit) {
313
+ function createTexture(gl, input, unit) {
314
+ const opts = resolveOptions(normalizeTextureInput(input));
315
+ const source = opts.src;
175
316
  const texture = initTexture(gl, unit);
176
317
  if (typeof source === "string") {
177
318
  gl.texImage2D(
@@ -203,10 +344,7 @@ function createTexture(gl, source, unit) {
203
344
  return;
204
345
  }
205
346
  uploadElement(gl, texture, unit, img);
206
- if (isPOT(img.width) && isPOT(img.height)) {
207
- gl.generateMipmap(gl.TEXTURE_2D);
208
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
209
- }
347
+ applyTextureParameters(gl, img.width, img.height, opts.wrap, opts.filter, opts.vflip);
210
348
  state2.width = img.width;
211
349
  state2.height = img.height;
212
350
  state2.loaded = true;
@@ -229,6 +367,7 @@ function createTexture(gl, source, unit) {
229
367
  };
230
368
  if (source.complete && source.naturalWidth > 0) {
231
369
  uploadElement(gl, texture, unit, source);
370
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
232
371
  state2.width = source.naturalWidth;
233
372
  state2.height = source.naturalHeight;
234
373
  return { state: state2, promise: null };
@@ -240,6 +379,7 @@ function createTexture(gl, source, unit) {
240
379
  return;
241
380
  }
242
381
  uploadElement(gl, texture, unit, source);
382
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
243
383
  state2.width = source.naturalWidth;
244
384
  state2.height = source.naturalHeight;
245
385
  state2.loaded = true;
@@ -254,6 +394,7 @@ function createTexture(gl, source, unit) {
254
394
  const h = source.videoHeight || 1;
255
395
  if (source.readyState >= 2) {
256
396
  uploadElement(gl, texture, unit, source);
397
+ applyTextureParameters(gl, w, h, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
257
398
  } else {
258
399
  gl.texImage2D(
259
400
  gl.TEXTURE_2D,
@@ -279,6 +420,7 @@ function createTexture(gl, source, unit) {
279
420
  return { state: state2, promise: null };
280
421
  }
281
422
  uploadElement(gl, texture, unit, source);
423
+ applyTextureParameters(gl, source.width, source.height, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
282
424
  const state = {
283
425
  texture,
284
426
  width: source.width,
@@ -325,63 +467,213 @@ function disposeTextures(gl, textures) {
325
467
  }
326
468
 
327
469
  // src/uniforms.ts
328
- function updateUniforms(state, delta, speed, mouse) {
329
- const { gl, locations } = state;
330
- state.time += delta * speed;
331
- if (locations.iTime) {
332
- gl.uniform1f(locations.iTime, state.time);
470
+ function setUniforms(gl, locations, time, delta, frame, width, height, mouse, channelRes) {
471
+ if (locations.iTime) gl.uniform1f(locations.iTime, time);
472
+ if (locations.iTimeDelta) gl.uniform1f(locations.iTimeDelta, delta);
473
+ if (locations.iFrame) gl.uniform1i(locations.iFrame, frame);
474
+ if (locations.iResolution) gl.uniform3f(locations.iResolution, width, height, 1);
475
+ if (locations.iMouse) {
476
+ const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
477
+ const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
478
+ gl.uniform4f(locations.iMouse, mouse.x, mouse.y, mz, mw);
333
479
  }
334
- if (locations.iTimeDelta) {
335
- gl.uniform1f(locations.iTimeDelta, delta);
480
+ if (locations.iChannelResolution && channelRes) {
481
+ gl.uniform3fv(locations.iChannelResolution, channelRes);
336
482
  }
483
+ if (locations.iDate) {
484
+ const now = /* @__PURE__ */ new Date();
485
+ const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
486
+ gl.uniform4f(locations.iDate, now.getFullYear(), now.getMonth(), now.getDate(), seconds);
487
+ }
488
+ }
489
+ function updateUniforms(state, delta, speed, mouse) {
490
+ state.time += delta * speed;
337
491
  state.frame++;
338
- if (locations.iFrame) {
339
- gl.uniform1i(locations.iFrame, state.frame);
492
+ const res = new Float32Array(12);
493
+ for (let i = 0; i < 4; i++) {
494
+ const tex = state.textures[i];
495
+ if (tex) {
496
+ res[i * 3] = tex.width;
497
+ res[i * 3 + 1] = tex.height;
498
+ res[i * 3 + 2] = 1;
499
+ }
340
500
  }
341
- if (locations.iResolution) {
342
- gl.uniform3f(
343
- locations.iResolution,
344
- gl.drawingBufferWidth,
345
- gl.drawingBufferHeight,
346
- 1
347
- );
501
+ setUniforms(
502
+ state.gl,
503
+ state.locations,
504
+ state.time,
505
+ delta,
506
+ state.frame,
507
+ state.gl.drawingBufferWidth,
508
+ state.gl.drawingBufferHeight,
509
+ mouse,
510
+ res
511
+ );
512
+ }
513
+
514
+ // src/multipass.ts
515
+ var PASS_ORDER = ["BufferA", "BufferB", "BufferC", "BufferD", "Image"];
516
+ var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
517
+ function isPassName(v) {
518
+ return typeof v === "string" && PASS_ORDER.includes(v);
519
+ }
520
+ function createPingPongTextures(gl, w, h) {
521
+ const textures = [];
522
+ for (let i = 0; i < 2; i++) {
523
+ const tex = gl.createTexture();
524
+ gl.bindTexture(gl.TEXTURE_2D, tex);
525
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
526
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
527
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
528
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
529
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
530
+ textures.push(tex);
348
531
  }
349
- if (locations.iMouse) {
350
- const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
351
- const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
352
- gl.uniform4f(locations.iMouse, mouse.x, mouse.y, mz, mw);
532
+ return textures;
533
+ }
534
+ function createFBO(gl, texture) {
535
+ const fbo = gl.createFramebuffer();
536
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
537
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
538
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
539
+ return fbo;
540
+ }
541
+ function createMultipassRenderer(gl, config, externalTextures) {
542
+ const w = gl.drawingBufferWidth || 1;
543
+ const h = gl.drawingBufferHeight || 1;
544
+ const passes = [];
545
+ for (const name of PASS_ORDER) {
546
+ const passConfig = config[name];
547
+ if (!passConfig) continue;
548
+ const program = createProgram(gl, passConfig.code);
549
+ if (typeof program === "string") return `${name}: ${program}`;
550
+ setupQuad(gl, program);
551
+ const locations = getUniformLocations(gl, program);
552
+ const isImage = name === "Image";
553
+ const pingPong = isImage ? null : createPingPongTextures(gl, w, h);
554
+ const fbo = isImage ? null : createFBO(gl, pingPong[0]);
555
+ const channelBindings = [null, null, null, null];
556
+ for (let i = 0; i < 4; i++) {
557
+ const input = passConfig[CHANNEL_KEYS[i]];
558
+ if (input == null) continue;
559
+ if (isPassName(input)) {
560
+ channelBindings[i] = { passRef: input };
561
+ } else {
562
+ channelBindings[i] = externalTextures[i];
563
+ }
564
+ }
565
+ passes.push({
566
+ name,
567
+ program,
568
+ locations,
569
+ fbo,
570
+ pingPong,
571
+ currentIdx: 0,
572
+ width: w,
573
+ height: h,
574
+ channelBindings
575
+ });
353
576
  }
354
- if (locations.iChannelResolution) {
355
- const res = new Float32Array(12);
577
+ if (passes.length === 0) return "No passes defined";
578
+ return passes;
579
+ }
580
+ function renderMultipass(gl, passes, delta, speed, mouse, sharedState) {
581
+ sharedState.time += delta * speed;
582
+ sharedState.frame++;
583
+ const passTextures = {};
584
+ for (const pass of passes) {
585
+ const isImage = pass.name === "Image";
586
+ if (pass.pingPong) {
587
+ const writeIdx = pass.currentIdx;
588
+ const readIdx = 1 - writeIdx;
589
+ gl.bindFramebuffer(gl.FRAMEBUFFER, pass.fbo);
590
+ gl.framebufferTexture2D(
591
+ gl.FRAMEBUFFER,
592
+ gl.COLOR_ATTACHMENT0,
593
+ gl.TEXTURE_2D,
594
+ pass.pingPong[writeIdx],
595
+ 0
596
+ );
597
+ passTextures[pass.name] = pass.pingPong[readIdx];
598
+ }
599
+ gl.bindFramebuffer(gl.FRAMEBUFFER, isImage ? null : pass.fbo);
600
+ gl.viewport(0, 0, isImage ? gl.drawingBufferWidth : pass.width, isImage ? gl.drawingBufferHeight : pass.height);
601
+ gl.useProgram(pass.program);
602
+ const tempTextures = [null, null, null, null];
356
603
  for (let i = 0; i < 4; i++) {
357
- const tex = state.textures[i];
358
- if (tex) {
359
- res[i * 3] = tex.width;
360
- res[i * 3 + 1] = tex.height;
361
- res[i * 3 + 2] = 1;
604
+ const binding = pass.channelBindings[i];
605
+ if (!binding) continue;
606
+ if ("passRef" in binding) {
607
+ const refTex = passTextures[binding.passRef];
608
+ if (refTex) {
609
+ gl.activeTexture(gl.TEXTURE0 + i);
610
+ gl.bindTexture(gl.TEXTURE_2D, refTex);
611
+ if (pass.locations.iChannel[i]) {
612
+ gl.uniform1i(pass.locations.iChannel[i], i);
613
+ }
614
+ }
615
+ } else {
616
+ tempTextures[i] = binding;
362
617
  }
363
618
  }
364
- gl.uniform3fv(locations.iChannelResolution, res);
619
+ bindTextures(gl, pass.locations.iChannel, tempTextures);
620
+ const channelRes = new Float32Array(12);
621
+ for (let i = 0; i < 4; i++) {
622
+ const binding = pass.channelBindings[i];
623
+ if (!binding) continue;
624
+ if ("passRef" in binding) {
625
+ const refPass = passes.find((p) => p.name === binding.passRef);
626
+ if (refPass) {
627
+ channelRes[i * 3] = refPass.width;
628
+ channelRes[i * 3 + 1] = refPass.height;
629
+ channelRes[i * 3 + 2] = 1;
630
+ }
631
+ } else {
632
+ channelRes[i * 3] = binding.width;
633
+ channelRes[i * 3 + 1] = binding.height;
634
+ channelRes[i * 3 + 2] = 1;
635
+ }
636
+ }
637
+ const vw = isImage ? gl.drawingBufferWidth : pass.width;
638
+ const vh = isImage ? gl.drawingBufferHeight : pass.height;
639
+ setUniforms(gl, pass.locations, sharedState.time, delta, sharedState.frame, vw, vh, mouse, channelRes);
640
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
641
+ if (pass.pingPong) {
642
+ pass.currentIdx = 1 - pass.currentIdx;
643
+ }
365
644
  }
366
- if (locations.iDate) {
367
- const now = /* @__PURE__ */ new Date();
368
- const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
369
- gl.uniform4f(
370
- locations.iDate,
371
- now.getFullYear(),
372
- now.getMonth(),
373
- // 0-based, matches Shadertoy
374
- now.getDate(),
375
- seconds
376
- );
645
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
646
+ }
647
+ function resizeFBOs(gl, passes, w, h) {
648
+ for (const pass of passes) {
649
+ if (!pass.pingPong) continue;
650
+ pass.width = w;
651
+ pass.height = h;
652
+ for (let i = 0; i < 2; i++) {
653
+ gl.bindTexture(gl.TEXTURE_2D, pass.pingPong[i]);
654
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
655
+ }
656
+ }
657
+ }
658
+ function disposeMultipass(gl, passes) {
659
+ for (const pass of passes) {
660
+ gl.deleteProgram(pass.program);
661
+ if (pass.fbo) gl.deleteFramebuffer(pass.fbo);
662
+ if (pass.pingPong) {
663
+ gl.deleteTexture(pass.pingPong[0]);
664
+ gl.deleteTexture(pass.pingPong[1]);
665
+ }
377
666
  }
378
667
  }
379
668
 
380
669
  // src/useShadertoy.ts
381
- var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
670
+ var CHANNEL_KEYS2 = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
382
671
  function useShadertoy({
383
672
  fragmentShader,
673
+ passes: passesProp,
384
674
  textures: texturesProp,
675
+ id,
676
+ apiKey,
385
677
  paused = false,
386
678
  speed = 1,
387
679
  pixelRatio,
@@ -391,11 +683,14 @@ function useShadertoy({
391
683
  }) {
392
684
  const canvasRef = (0, import_react.useRef)(null);
393
685
  const rendererRef = (0, import_react.useRef)(null);
686
+ const multipassRef = (0, import_react.useRef)(null);
394
687
  const rafRef = (0, import_react.useRef)(0);
395
688
  const pausedRef = (0, import_react.useRef)(paused);
396
689
  const speedRef = (0, import_react.useRef)(speed);
397
690
  const [isReady, setIsReady] = (0, import_react.useState)(false);
398
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 });
399
694
  const mouseState = (0, import_react.useRef)({
400
695
  x: 0,
401
696
  y: 0,
@@ -403,25 +698,68 @@ function useShadertoy({
403
698
  clickY: 0,
404
699
  pressed: false
405
700
  });
701
+ const sharedState = (0, import_react.useRef)({ time: 0, frame: 0 });
406
702
  pausedRef.current = paused;
407
703
  speedRef.current = speed;
408
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;
409
741
  const canvas = canvasRef.current;
410
742
  if (!canvas) return;
411
- const result = createRenderer(canvas, fragmentShader);
412
- if (typeof result === "string") {
413
- setError(result);
414
- onError?.(result);
743
+ sharedState.current = { time: 0, frame: 0 };
744
+ const gl = canvas.getContext("webgl2", {
745
+ antialias: false,
746
+ alpha: true,
747
+ premultipliedAlpha: false
748
+ });
749
+ if (!gl) {
750
+ const msg = "WebGL2 not supported";
751
+ setError(msg);
752
+ onError?.(msg);
415
753
  return;
416
754
  }
417
- rendererRef.current = result;
755
+ const externalTextures = [null, null, null, null];
418
756
  const texturePromises = [];
419
- if (texturesProp) {
757
+ if (effectiveTextures) {
420
758
  for (let i = 0; i < 4; i++) {
421
- const src = texturesProp[CHANNEL_KEYS[i]];
759
+ const src = effectiveTextures[CHANNEL_KEYS2[i]];
422
760
  if (src != null) {
423
- const { state, promise } = createTexture(result.gl, src, i);
424
- result.textures[i] = state;
761
+ const { state, promise } = createTexture(gl, src, i);
762
+ externalTextures[i] = state;
425
763
  if (promise) texturePromises.push(promise);
426
764
  }
427
765
  }
@@ -431,41 +769,88 @@ function useShadertoy({
431
769
  setError(null);
432
770
  onLoad?.();
433
771
  };
434
- if (texturePromises.length > 0) {
435
- Promise.all(texturePromises).then(() => {
436
- if (rendererRef.current) markReady();
437
- }).catch((err) => {
438
- const msg = err instanceof Error ? err.message : "Texture load failed";
439
- setError(msg);
440
- onError?.(msg);
441
- });
442
- } else {
443
- markReady();
444
- }
445
- let lastTimestamp = 0;
446
- const loop = (timestamp) => {
447
- const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
448
- lastTimestamp = timestamp;
449
- if (!pausedRef.current && rendererRef.current) {
450
- const r = rendererRef.current;
451
- updateDynamicTextures(r.gl, r.textures);
452
- bindTextures(r.gl, r.locations.iChannel, r.textures);
453
- updateUniforms(r, delta, speedRef.current, mouseState.current);
454
- render(r);
772
+ const handleError = (msg) => {
773
+ setError(msg);
774
+ onError?.(msg);
775
+ };
776
+ if (isMultipass) {
777
+ const passResult = createMultipassRenderer(gl, effectivePasses, externalTextures);
778
+ if (typeof passResult === "string") {
779
+ handleError(passResult);
780
+ return;
455
781
  }
782
+ multipassRef.current = passResult;
783
+ rendererRef.current = null;
784
+ if (texturePromises.length > 0) {
785
+ Promise.all(texturePromises).then(() => {
786
+ if (multipassRef.current) markReady();
787
+ }).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
788
+ } else {
789
+ markReady();
790
+ }
791
+ let lastTimestamp = 0;
792
+ const loop = (timestamp) => {
793
+ const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
794
+ lastTimestamp = timestamp;
795
+ if (!pausedRef.current && multipassRef.current) {
796
+ updateDynamicTextures(gl, externalTextures);
797
+ renderMultipass(gl, multipassRef.current, delta, speedRef.current, mouseState.current, sharedState.current);
798
+ }
799
+ rafRef.current = requestAnimationFrame(loop);
800
+ };
456
801
  rafRef.current = requestAnimationFrame(loop);
457
- };
458
- rafRef.current = requestAnimationFrame(loop);
459
- return () => {
460
- cancelAnimationFrame(rafRef.current);
461
- if (rendererRef.current) {
462
- disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
463
- dispose(rendererRef.current);
464
- rendererRef.current = null;
802
+ return () => {
803
+ cancelAnimationFrame(rafRef.current);
804
+ if (multipassRef.current) {
805
+ disposeMultipass(gl, multipassRef.current);
806
+ multipassRef.current = null;
807
+ }
808
+ disposeTextures(gl, externalTextures);
809
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
810
+ setIsReady(false);
811
+ };
812
+ } else {
813
+ const shaderCode = effectiveShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
814
+ const result = createRenderer(canvas, shaderCode);
815
+ if (typeof result === "string") {
816
+ handleError(result);
817
+ return;
465
818
  }
466
- setIsReady(false);
467
- };
468
- }, [fragmentShader, texturesProp, onError, onLoad]);
819
+ rendererRef.current = result;
820
+ multipassRef.current = null;
821
+ result.textures = externalTextures;
822
+ if (texturePromises.length > 0) {
823
+ Promise.all(texturePromises).then(() => {
824
+ if (rendererRef.current) markReady();
825
+ }).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
826
+ } else {
827
+ markReady();
828
+ }
829
+ let lastTimestamp = 0;
830
+ const loop = (timestamp) => {
831
+ const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
832
+ lastTimestamp = timestamp;
833
+ if (!pausedRef.current && rendererRef.current) {
834
+ const r = rendererRef.current;
835
+ updateDynamicTextures(r.gl, r.textures);
836
+ bindTextures(r.gl, r.locations.iChannel, r.textures);
837
+ updateUniforms(r, delta, speedRef.current, mouseState.current);
838
+ render(r);
839
+ }
840
+ rafRef.current = requestAnimationFrame(loop);
841
+ };
842
+ rafRef.current = requestAnimationFrame(loop);
843
+ return () => {
844
+ cancelAnimationFrame(rafRef.current);
845
+ if (rendererRef.current) {
846
+ disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
847
+ dispose(rendererRef.current);
848
+ rendererRef.current = null;
849
+ }
850
+ setIsReady(false);
851
+ };
852
+ }
853
+ }, [effectiveShader, effectivePasses, effectiveTextures, resolved, onError, onLoad]);
469
854
  (0, import_react.useEffect)(() => {
470
855
  const canvas = canvasRef.current;
471
856
  if (!canvas) return;
@@ -473,8 +858,14 @@ function useShadertoy({
473
858
  const observer = new ResizeObserver((entries) => {
474
859
  for (const entry of entries) {
475
860
  const { width, height } = entry.contentRect;
476
- canvas.width = Math.round(width * dpr);
477
- canvas.height = Math.round(height * dpr);
861
+ const w = Math.round(width * dpr);
862
+ const h = Math.round(height * dpr);
863
+ canvas.width = w;
864
+ canvas.height = h;
865
+ if (multipassRef.current) {
866
+ const gl = canvas.getContext("webgl2");
867
+ if (gl) resizeFBOs(gl, multipassRef.current, w, h);
868
+ }
478
869
  }
479
870
  });
480
871
  observer.observe(canvas);
@@ -541,14 +932,18 @@ function useShadertoy({
541
932
  const resume = (0, import_react.useCallback)(() => {
542
933
  pausedRef.current = false;
543
934
  }, []);
544
- return { canvasRef, isReady, error, pause, resume };
935
+ return { canvasRef, isReady, error, pause, resume, meta };
545
936
  }
546
937
 
547
938
  // src/Shadertoy.tsx
548
939
  var import_jsx_runtime = require("react/jsx-runtime");
549
940
  function Shadertoy({
550
941
  fragmentShader,
942
+ passes,
551
943
  textures,
944
+ id,
945
+ apiKey,
946
+ showLicense,
552
947
  style,
553
948
  className,
554
949
  paused,
@@ -558,9 +953,12 @@ function Shadertoy({
558
953
  onError,
559
954
  onLoad
560
955
  }) {
561
- const { canvasRef } = useShadertoy({
956
+ const { canvasRef, meta } = useShadertoy({
562
957
  fragmentShader,
958
+ passes,
563
959
  textures,
960
+ id,
961
+ apiKey,
564
962
  paused,
565
963
  speed,
566
964
  pixelRatio,
@@ -568,6 +966,35 @@ function Shadertoy({
568
966
  onError,
569
967
  onLoad
570
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
+ }
571
998
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
572
999
  "canvas",
573
1000
  {
@@ -580,5 +1007,7 @@ function Shadertoy({
580
1007
  // Annotate the CommonJS export names for ESM import in node:
581
1008
  0 && (module.exports = {
582
1009
  Shadertoy,
1010
+ apiToConfig,
1011
+ fetchShader,
583
1012
  useShadertoy
584
1013
  });