react-shadertoy 0.3.0 → 0.5.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
@@ -43,14 +43,16 @@ var QUAD_VERTICES = new Float32Array([
43
43
  1,
44
44
  1
45
45
  ]);
46
- var VERTEX_SHADER = `
47
- attribute vec2 position;
46
+ var VERTEX_SHADER = `#version 300 es
47
+ in vec2 position;
48
48
  void main() {
49
49
  gl_Position = vec4(position, 0.0, 1.0);
50
50
  }
51
51
  `;
52
- function wrapFragmentShader(shader) {
53
- return `precision highp float;
52
+ var FRAGMENT_PREAMBLE = `#version 300 es
53
+ precision highp float;
54
+
55
+ out vec4 _fragColor;
54
56
 
55
57
  uniform vec3 iResolution;
56
58
  uniform float iTime;
@@ -64,13 +66,16 @@ uniform sampler2D iChannel2;
64
66
  uniform sampler2D iChannel3;
65
67
  uniform vec3 iChannelResolution[4];
66
68
 
67
- // Shadertoy compatibility: texture() is GLSL 300 es, WebGL1 uses texture2D()
68
- #define texture texture2D
69
+ // Shadertoy compat: older shaders may use texture2D()
70
+ #define texture2D texture
69
71
 
70
- ${shader}
72
+ `;
73
+ function wrapFragmentShader(shader) {
74
+ return FRAGMENT_PREAMBLE + shader + `
71
75
 
72
76
  void main() {
73
- mainImage(gl_FragColor, gl_FragCoord.xy);
77
+ mainImage(_fragColor, gl_FragCoord.xy);
78
+ _fragColor.a = 1.0;
74
79
  }
75
80
  `;
76
81
  }
@@ -86,13 +91,7 @@ function compileShader(gl, type, source) {
86
91
  }
87
92
  return shader;
88
93
  }
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";
94
+ function createProgram(gl, fragmentShader) {
96
95
  const vert = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
97
96
  if (typeof vert === "string") return vert;
98
97
  const frag = compileShader(gl, gl.FRAGMENT_SHADER, wrapFragmentShader(fragmentShader));
@@ -109,13 +108,10 @@ function createRenderer(canvas, fragmentShader) {
109
108
  }
110
109
  gl.deleteShader(vert);
111
110
  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 = {
111
+ return program;
112
+ }
113
+ function getUniformLocations(gl, program) {
114
+ return {
119
115
  iResolution: gl.getUniformLocation(program, "iResolution"),
120
116
  iTime: gl.getUniformLocation(program, "iTime"),
121
117
  iTimeDelta: gl.getUniformLocation(program, "iTimeDelta"),
@@ -130,6 +126,26 @@ function createRenderer(canvas, fragmentShader) {
130
126
  ],
131
127
  iChannelResolution: gl.getUniformLocation(program, "iChannelResolution")
132
128
  };
129
+ }
130
+ function setupQuad(gl, program) {
131
+ const buffer = gl.createBuffer();
132
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
133
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
134
+ const positionLoc = gl.getAttribLocation(program, "position");
135
+ gl.enableVertexAttribArray(positionLoc);
136
+ gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
137
+ }
138
+ function createRenderer(canvas, fragmentShader) {
139
+ const gl = canvas.getContext("webgl2", {
140
+ antialias: false,
141
+ alpha: true,
142
+ premultipliedAlpha: false
143
+ });
144
+ if (!gl) return "WebGL2 not supported";
145
+ const program = createProgram(gl, fragmentShader);
146
+ if (typeof program === "string") return program;
147
+ setupQuad(gl, program);
148
+ const locations = getUniformLocations(gl, program);
133
149
  gl.useProgram(program);
134
150
  return {
135
151
  gl,
@@ -153,8 +169,19 @@ function dispose(state) {
153
169
  }
154
170
 
155
171
  // src/textures.ts
156
- function isPOT(v) {
157
- return (v & v - 1) === 0 && v > 0;
172
+ function normalizeTextureInput(input) {
173
+ if (typeof input === "object" && input !== null && "src" in input) {
174
+ return input;
175
+ }
176
+ return { src: input };
177
+ }
178
+ function resolveOptions(opts) {
179
+ return {
180
+ src: opts.src,
181
+ wrap: opts.wrap ?? "clamp",
182
+ filter: opts.filter ?? "mipmap",
183
+ vflip: opts.vflip ?? true
184
+ };
158
185
  }
159
186
  function initTexture(gl, unit) {
160
187
  const texture = gl.createTexture();
@@ -166,12 +193,32 @@ function initTexture(gl, unit) {
166
193
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
167
194
  return texture;
168
195
  }
196
+ function applyTextureParameters(gl, w, h, wrap, filter, vflip) {
197
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, vflip ? 1 : 0);
198
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
199
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
200
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
201
+ if (filter === "mipmap") {
202
+ gl.generateMipmap(gl.TEXTURE_2D);
203
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
204
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
205
+ } else if (filter === "nearest") {
206
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
207
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
208
+ } else {
209
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
210
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
211
+ }
212
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
213
+ }
169
214
  function uploadElement(gl, texture, unit, el) {
170
215
  gl.activeTexture(gl.TEXTURE0 + unit);
171
216
  gl.bindTexture(gl.TEXTURE_2D, texture);
172
217
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
173
218
  }
174
- function createTexture(gl, source, unit) {
219
+ function createTexture(gl, input, unit) {
220
+ const opts = resolveOptions(normalizeTextureInput(input));
221
+ const source = opts.src;
175
222
  const texture = initTexture(gl, unit);
176
223
  if (typeof source === "string") {
177
224
  gl.texImage2D(
@@ -203,10 +250,7 @@ function createTexture(gl, source, unit) {
203
250
  return;
204
251
  }
205
252
  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
- }
253
+ applyTextureParameters(gl, img.width, img.height, opts.wrap, opts.filter, opts.vflip);
210
254
  state2.width = img.width;
211
255
  state2.height = img.height;
212
256
  state2.loaded = true;
@@ -229,6 +273,7 @@ function createTexture(gl, source, unit) {
229
273
  };
230
274
  if (source.complete && source.naturalWidth > 0) {
231
275
  uploadElement(gl, texture, unit, source);
276
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
232
277
  state2.width = source.naturalWidth;
233
278
  state2.height = source.naturalHeight;
234
279
  return { state: state2, promise: null };
@@ -240,6 +285,7 @@ function createTexture(gl, source, unit) {
240
285
  return;
241
286
  }
242
287
  uploadElement(gl, texture, unit, source);
288
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
243
289
  state2.width = source.naturalWidth;
244
290
  state2.height = source.naturalHeight;
245
291
  state2.loaded = true;
@@ -254,6 +300,7 @@ function createTexture(gl, source, unit) {
254
300
  const h = source.videoHeight || 1;
255
301
  if (source.readyState >= 2) {
256
302
  uploadElement(gl, texture, unit, source);
303
+ applyTextureParameters(gl, w, h, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
257
304
  } else {
258
305
  gl.texImage2D(
259
306
  gl.TEXTURE_2D,
@@ -279,6 +326,7 @@ function createTexture(gl, source, unit) {
279
326
  return { state: state2, promise: null };
280
327
  }
281
328
  uploadElement(gl, texture, unit, source);
329
+ applyTextureParameters(gl, source.width, source.height, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
282
330
  const state = {
283
331
  texture,
284
332
  width: source.width,
@@ -325,62 +373,210 @@ function disposeTextures(gl, textures) {
325
373
  }
326
374
 
327
375
  // 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);
376
+ function setUniforms(gl, locations, time, delta, frame, width, height, mouse, channelRes) {
377
+ if (locations.iTime) gl.uniform1f(locations.iTime, time);
378
+ if (locations.iTimeDelta) gl.uniform1f(locations.iTimeDelta, delta);
379
+ if (locations.iFrame) gl.uniform1i(locations.iFrame, frame);
380
+ if (locations.iResolution) gl.uniform3f(locations.iResolution, width, height, 1);
381
+ if (locations.iMouse) {
382
+ const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
383
+ const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
384
+ gl.uniform4f(locations.iMouse, mouse.x, mouse.y, mz, mw);
333
385
  }
334
- if (locations.iTimeDelta) {
335
- gl.uniform1f(locations.iTimeDelta, delta);
386
+ if (locations.iChannelResolution && channelRes) {
387
+ gl.uniform3fv(locations.iChannelResolution, channelRes);
336
388
  }
389
+ if (locations.iDate) {
390
+ const now = /* @__PURE__ */ new Date();
391
+ const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
392
+ gl.uniform4f(locations.iDate, now.getFullYear(), now.getMonth(), now.getDate(), seconds);
393
+ }
394
+ }
395
+ function updateUniforms(state, delta, speed, mouse) {
396
+ state.time += delta * speed;
337
397
  state.frame++;
338
- if (locations.iFrame) {
339
- gl.uniform1i(locations.iFrame, state.frame);
398
+ const res = new Float32Array(12);
399
+ for (let i = 0; i < 4; i++) {
400
+ const tex = state.textures[i];
401
+ if (tex) {
402
+ res[i * 3] = tex.width;
403
+ res[i * 3 + 1] = tex.height;
404
+ res[i * 3 + 2] = 1;
405
+ }
340
406
  }
341
- if (locations.iResolution) {
342
- gl.uniform3f(
343
- locations.iResolution,
344
- gl.drawingBufferWidth,
345
- gl.drawingBufferHeight,
346
- 1
347
- );
407
+ setUniforms(
408
+ state.gl,
409
+ state.locations,
410
+ state.time,
411
+ delta,
412
+ state.frame,
413
+ state.gl.drawingBufferWidth,
414
+ state.gl.drawingBufferHeight,
415
+ mouse,
416
+ res
417
+ );
418
+ }
419
+
420
+ // src/multipass.ts
421
+ var PASS_ORDER = ["BufferA", "BufferB", "BufferC", "BufferD", "Image"];
422
+ var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
423
+ function isPassName(v) {
424
+ return typeof v === "string" && PASS_ORDER.includes(v);
425
+ }
426
+ function createPingPongTextures(gl, w, h) {
427
+ const textures = [];
428
+ for (let i = 0; i < 2; i++) {
429
+ const tex = gl.createTexture();
430
+ gl.bindTexture(gl.TEXTURE_2D, tex);
431
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
432
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
433
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
434
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
435
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
436
+ textures.push(tex);
348
437
  }
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);
438
+ return textures;
439
+ }
440
+ function createFBO(gl, texture) {
441
+ const fbo = gl.createFramebuffer();
442
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
443
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
444
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
445
+ return fbo;
446
+ }
447
+ function createMultipassRenderer(gl, config, externalTextures) {
448
+ const w = gl.drawingBufferWidth || 1;
449
+ const h = gl.drawingBufferHeight || 1;
450
+ const passes = [];
451
+ for (const name of PASS_ORDER) {
452
+ const passConfig = config[name];
453
+ if (!passConfig) continue;
454
+ const program = createProgram(gl, passConfig.code);
455
+ if (typeof program === "string") return `${name}: ${program}`;
456
+ setupQuad(gl, program);
457
+ const locations = getUniformLocations(gl, program);
458
+ const isImage = name === "Image";
459
+ const pingPong = isImage ? null : createPingPongTextures(gl, w, h);
460
+ const fbo = isImage ? null : createFBO(gl, pingPong[0]);
461
+ const channelBindings = [null, null, null, null];
462
+ for (let i = 0; i < 4; i++) {
463
+ const input = passConfig[CHANNEL_KEYS[i]];
464
+ if (input == null) continue;
465
+ if (isPassName(input)) {
466
+ channelBindings[i] = { passRef: input };
467
+ } else {
468
+ channelBindings[i] = externalTextures[i];
469
+ }
470
+ }
471
+ passes.push({
472
+ name,
473
+ program,
474
+ locations,
475
+ fbo,
476
+ pingPong,
477
+ currentIdx: 0,
478
+ width: w,
479
+ height: h,
480
+ channelBindings
481
+ });
353
482
  }
354
- if (locations.iChannelResolution) {
355
- const res = new Float32Array(12);
483
+ if (passes.length === 0) return "No passes defined";
484
+ return passes;
485
+ }
486
+ function renderMultipass(gl, passes, delta, speed, mouse, sharedState) {
487
+ sharedState.time += delta * speed;
488
+ sharedState.frame++;
489
+ const passTextures = {};
490
+ for (const pass of passes) {
491
+ const isImage = pass.name === "Image";
492
+ if (pass.pingPong) {
493
+ const writeIdx = pass.currentIdx;
494
+ const readIdx = 1 - writeIdx;
495
+ gl.bindFramebuffer(gl.FRAMEBUFFER, pass.fbo);
496
+ gl.framebufferTexture2D(
497
+ gl.FRAMEBUFFER,
498
+ gl.COLOR_ATTACHMENT0,
499
+ gl.TEXTURE_2D,
500
+ pass.pingPong[writeIdx],
501
+ 0
502
+ );
503
+ passTextures[pass.name] = pass.pingPong[readIdx];
504
+ }
505
+ gl.bindFramebuffer(gl.FRAMEBUFFER, isImage ? null : pass.fbo);
506
+ gl.viewport(0, 0, isImage ? gl.drawingBufferWidth : pass.width, isImage ? gl.drawingBufferHeight : pass.height);
507
+ gl.useProgram(pass.program);
508
+ const tempTextures = [null, null, null, null];
509
+ for (let i = 0; i < 4; i++) {
510
+ const binding = pass.channelBindings[i];
511
+ if (!binding) continue;
512
+ if ("passRef" in binding) {
513
+ const refTex = passTextures[binding.passRef];
514
+ if (refTex) {
515
+ gl.activeTexture(gl.TEXTURE0 + i);
516
+ gl.bindTexture(gl.TEXTURE_2D, refTex);
517
+ if (pass.locations.iChannel[i]) {
518
+ gl.uniform1i(pass.locations.iChannel[i], i);
519
+ }
520
+ }
521
+ } else {
522
+ tempTextures[i] = binding;
523
+ }
524
+ }
525
+ bindTextures(gl, pass.locations.iChannel, tempTextures);
526
+ const channelRes = new Float32Array(12);
356
527
  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;
528
+ const binding = pass.channelBindings[i];
529
+ if (!binding) continue;
530
+ if ("passRef" in binding) {
531
+ const refPass = passes.find((p) => p.name === binding.passRef);
532
+ if (refPass) {
533
+ channelRes[i * 3] = refPass.width;
534
+ channelRes[i * 3 + 1] = refPass.height;
535
+ channelRes[i * 3 + 2] = 1;
536
+ }
537
+ } else {
538
+ channelRes[i * 3] = binding.width;
539
+ channelRes[i * 3 + 1] = binding.height;
540
+ channelRes[i * 3 + 2] = 1;
362
541
  }
363
542
  }
364
- gl.uniform3fv(locations.iChannelResolution, res);
543
+ const vw = isImage ? gl.drawingBufferWidth : pass.width;
544
+ const vh = isImage ? gl.drawingBufferHeight : pass.height;
545
+ setUniforms(gl, pass.locations, sharedState.time, delta, sharedState.frame, vw, vh, mouse, channelRes);
546
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
547
+ if (pass.pingPong) {
548
+ pass.currentIdx = 1 - pass.currentIdx;
549
+ }
365
550
  }
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
- );
551
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
552
+ }
553
+ function resizeFBOs(gl, passes, w, h) {
554
+ for (const pass of passes) {
555
+ if (!pass.pingPong) continue;
556
+ pass.width = w;
557
+ pass.height = h;
558
+ for (let i = 0; i < 2; i++) {
559
+ gl.bindTexture(gl.TEXTURE_2D, pass.pingPong[i]);
560
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
561
+ }
562
+ }
563
+ }
564
+ function disposeMultipass(gl, passes) {
565
+ for (const pass of passes) {
566
+ gl.deleteProgram(pass.program);
567
+ if (pass.fbo) gl.deleteFramebuffer(pass.fbo);
568
+ if (pass.pingPong) {
569
+ gl.deleteTexture(pass.pingPong[0]);
570
+ gl.deleteTexture(pass.pingPong[1]);
571
+ }
377
572
  }
378
573
  }
379
574
 
380
575
  // src/useShadertoy.ts
381
- var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
576
+ var CHANNEL_KEYS2 = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
382
577
  function useShadertoy({
383
578
  fragmentShader,
579
+ passes: passesProp,
384
580
  textures: texturesProp,
385
581
  paused = false,
386
582
  speed = 1,
@@ -391,6 +587,7 @@ function useShadertoy({
391
587
  }) {
392
588
  const canvasRef = (0, import_react.useRef)(null);
393
589
  const rendererRef = (0, import_react.useRef)(null);
590
+ const multipassRef = (0, import_react.useRef)(null);
394
591
  const rafRef = (0, import_react.useRef)(0);
395
592
  const pausedRef = (0, import_react.useRef)(paused);
396
593
  const speedRef = (0, import_react.useRef)(speed);
@@ -403,25 +600,33 @@ function useShadertoy({
403
600
  clickY: 0,
404
601
  pressed: false
405
602
  });
603
+ const sharedState = (0, import_react.useRef)({ time: 0, frame: 0 });
406
604
  pausedRef.current = paused;
407
605
  speedRef.current = speed;
606
+ const isMultipass = !!passesProp;
408
607
  (0, import_react.useEffect)(() => {
409
608
  const canvas = canvasRef.current;
410
609
  if (!canvas) return;
411
- const result = createRenderer(canvas, fragmentShader);
412
- if (typeof result === "string") {
413
- setError(result);
414
- onError?.(result);
610
+ sharedState.current = { time: 0, frame: 0 };
611
+ const gl = canvas.getContext("webgl2", {
612
+ antialias: false,
613
+ alpha: true,
614
+ premultipliedAlpha: false
615
+ });
616
+ if (!gl) {
617
+ const msg = "WebGL2 not supported";
618
+ setError(msg);
619
+ onError?.(msg);
415
620
  return;
416
621
  }
417
- rendererRef.current = result;
622
+ const externalTextures = [null, null, null, null];
418
623
  const texturePromises = [];
419
624
  if (texturesProp) {
420
625
  for (let i = 0; i < 4; i++) {
421
- const src = texturesProp[CHANNEL_KEYS[i]];
626
+ const src = texturesProp[CHANNEL_KEYS2[i]];
422
627
  if (src != null) {
423
- const { state, promise } = createTexture(result.gl, src, i);
424
- result.textures[i] = state;
628
+ const { state, promise } = createTexture(gl, src, i);
629
+ externalTextures[i] = state;
425
630
  if (promise) texturePromises.push(promise);
426
631
  }
427
632
  }
@@ -431,41 +636,88 @@ function useShadertoy({
431
636
  setError(null);
432
637
  onLoad?.();
433
638
  };
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);
639
+ const handleError = (msg) => {
640
+ setError(msg);
641
+ onError?.(msg);
642
+ };
643
+ if (isMultipass) {
644
+ const passResult = createMultipassRenderer(gl, passesProp, externalTextures);
645
+ if (typeof passResult === "string") {
646
+ handleError(passResult);
647
+ return;
455
648
  }
649
+ multipassRef.current = passResult;
650
+ rendererRef.current = null;
651
+ if (texturePromises.length > 0) {
652
+ Promise.all(texturePromises).then(() => {
653
+ if (multipassRef.current) markReady();
654
+ }).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
655
+ } else {
656
+ markReady();
657
+ }
658
+ let lastTimestamp = 0;
659
+ const loop = (timestamp) => {
660
+ const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
661
+ lastTimestamp = timestamp;
662
+ if (!pausedRef.current && multipassRef.current) {
663
+ updateDynamicTextures(gl, externalTextures);
664
+ renderMultipass(gl, multipassRef.current, delta, speedRef.current, mouseState.current, sharedState.current);
665
+ }
666
+ rafRef.current = requestAnimationFrame(loop);
667
+ };
456
668
  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;
669
+ return () => {
670
+ cancelAnimationFrame(rafRef.current);
671
+ if (multipassRef.current) {
672
+ disposeMultipass(gl, multipassRef.current);
673
+ multipassRef.current = null;
674
+ }
675
+ disposeTextures(gl, externalTextures);
676
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
677
+ setIsReady(false);
678
+ };
679
+ } else {
680
+ const shaderCode = fragmentShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
681
+ const result = createRenderer(canvas, shaderCode);
682
+ if (typeof result === "string") {
683
+ handleError(result);
684
+ return;
465
685
  }
466
- setIsReady(false);
467
- };
468
- }, [fragmentShader, texturesProp, onError, onLoad]);
686
+ rendererRef.current = result;
687
+ multipassRef.current = null;
688
+ result.textures = externalTextures;
689
+ if (texturePromises.length > 0) {
690
+ Promise.all(texturePromises).then(() => {
691
+ if (rendererRef.current) markReady();
692
+ }).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
693
+ } else {
694
+ markReady();
695
+ }
696
+ let lastTimestamp = 0;
697
+ const loop = (timestamp) => {
698
+ const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
699
+ lastTimestamp = timestamp;
700
+ if (!pausedRef.current && rendererRef.current) {
701
+ const r = rendererRef.current;
702
+ updateDynamicTextures(r.gl, r.textures);
703
+ bindTextures(r.gl, r.locations.iChannel, r.textures);
704
+ updateUniforms(r, delta, speedRef.current, mouseState.current);
705
+ render(r);
706
+ }
707
+ rafRef.current = requestAnimationFrame(loop);
708
+ };
709
+ rafRef.current = requestAnimationFrame(loop);
710
+ return () => {
711
+ cancelAnimationFrame(rafRef.current);
712
+ if (rendererRef.current) {
713
+ disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
714
+ dispose(rendererRef.current);
715
+ rendererRef.current = null;
716
+ }
717
+ setIsReady(false);
718
+ };
719
+ }
720
+ }, [fragmentShader, passesProp, texturesProp, onError, onLoad]);
469
721
  (0, import_react.useEffect)(() => {
470
722
  const canvas = canvasRef.current;
471
723
  if (!canvas) return;
@@ -473,8 +725,14 @@ function useShadertoy({
473
725
  const observer = new ResizeObserver((entries) => {
474
726
  for (const entry of entries) {
475
727
  const { width, height } = entry.contentRect;
476
- canvas.width = Math.round(width * dpr);
477
- canvas.height = Math.round(height * dpr);
728
+ const w = Math.round(width * dpr);
729
+ const h = Math.round(height * dpr);
730
+ canvas.width = w;
731
+ canvas.height = h;
732
+ if (multipassRef.current) {
733
+ const gl = canvas.getContext("webgl2");
734
+ if (gl) resizeFBOs(gl, multipassRef.current, w, h);
735
+ }
478
736
  }
479
737
  });
480
738
  observer.observe(canvas);
@@ -548,6 +806,7 @@ function useShadertoy({
548
806
  var import_jsx_runtime = require("react/jsx-runtime");
549
807
  function Shadertoy({
550
808
  fragmentShader,
809
+ passes,
551
810
  textures,
552
811
  style,
553
812
  className,
@@ -560,6 +819,7 @@ function Shadertoy({
560
819
  }) {
561
820
  const { canvasRef } = useShadertoy({
562
821
  fragmentShader,
822
+ passes,
563
823
  textures,
564
824
  paused,
565
825
  speed,