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.mjs CHANGED
@@ -16,14 +16,16 @@ var QUAD_VERTICES = new Float32Array([
16
16
  1,
17
17
  1
18
18
  ]);
19
- var VERTEX_SHADER = `
20
- attribute vec2 position;
19
+ var VERTEX_SHADER = `#version 300 es
20
+ in vec2 position;
21
21
  void main() {
22
22
  gl_Position = vec4(position, 0.0, 1.0);
23
23
  }
24
24
  `;
25
- function wrapFragmentShader(shader) {
26
- return `precision highp float;
25
+ var FRAGMENT_PREAMBLE = `#version 300 es
26
+ precision highp float;
27
+
28
+ out vec4 _fragColor;
27
29
 
28
30
  uniform vec3 iResolution;
29
31
  uniform float iTime;
@@ -37,13 +39,16 @@ uniform sampler2D iChannel2;
37
39
  uniform sampler2D iChannel3;
38
40
  uniform vec3 iChannelResolution[4];
39
41
 
40
- // Shadertoy compatibility: texture() is GLSL 300 es, WebGL1 uses texture2D()
41
- #define texture texture2D
42
+ // Shadertoy compat: older shaders may use texture2D()
43
+ #define texture2D texture
42
44
 
43
- ${shader}
45
+ `;
46
+ function wrapFragmentShader(shader) {
47
+ return FRAGMENT_PREAMBLE + shader + `
44
48
 
45
49
  void main() {
46
- mainImage(gl_FragColor, gl_FragCoord.xy);
50
+ mainImage(_fragColor, gl_FragCoord.xy);
51
+ _fragColor.a = 1.0;
47
52
  }
48
53
  `;
49
54
  }
@@ -59,13 +64,7 @@ function compileShader(gl, type, source) {
59
64
  }
60
65
  return shader;
61
66
  }
62
- function createRenderer(canvas, fragmentShader) {
63
- const gl = canvas.getContext("webgl", {
64
- antialias: false,
65
- alpha: true,
66
- premultipliedAlpha: false
67
- });
68
- if (!gl) return "WebGL not supported";
67
+ function createProgram(gl, fragmentShader) {
69
68
  const vert = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
70
69
  if (typeof vert === "string") return vert;
71
70
  const frag = compileShader(gl, gl.FRAGMENT_SHADER, wrapFragmentShader(fragmentShader));
@@ -82,13 +81,10 @@ function createRenderer(canvas, fragmentShader) {
82
81
  }
83
82
  gl.deleteShader(vert);
84
83
  gl.deleteShader(frag);
85
- const buffer = gl.createBuffer();
86
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
87
- gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
88
- const positionLoc = gl.getAttribLocation(program, "position");
89
- gl.enableVertexAttribArray(positionLoc);
90
- gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
91
- const locations = {
84
+ return program;
85
+ }
86
+ function getUniformLocations(gl, program) {
87
+ return {
92
88
  iResolution: gl.getUniformLocation(program, "iResolution"),
93
89
  iTime: gl.getUniformLocation(program, "iTime"),
94
90
  iTimeDelta: gl.getUniformLocation(program, "iTimeDelta"),
@@ -103,6 +99,26 @@ function createRenderer(canvas, fragmentShader) {
103
99
  ],
104
100
  iChannelResolution: gl.getUniformLocation(program, "iChannelResolution")
105
101
  };
102
+ }
103
+ function setupQuad(gl, program) {
104
+ const buffer = gl.createBuffer();
105
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
106
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
107
+ const positionLoc = gl.getAttribLocation(program, "position");
108
+ gl.enableVertexAttribArray(positionLoc);
109
+ gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
110
+ }
111
+ function createRenderer(canvas, fragmentShader) {
112
+ const gl = canvas.getContext("webgl2", {
113
+ antialias: false,
114
+ alpha: true,
115
+ premultipliedAlpha: false
116
+ });
117
+ if (!gl) return "WebGL2 not supported";
118
+ const program = createProgram(gl, fragmentShader);
119
+ if (typeof program === "string") return program;
120
+ setupQuad(gl, program);
121
+ const locations = getUniformLocations(gl, program);
106
122
  gl.useProgram(program);
107
123
  return {
108
124
  gl,
@@ -126,8 +142,19 @@ function dispose(state) {
126
142
  }
127
143
 
128
144
  // src/textures.ts
129
- function isPOT(v) {
130
- return (v & v - 1) === 0 && v > 0;
145
+ function normalizeTextureInput(input) {
146
+ if (typeof input === "object" && input !== null && "src" in input) {
147
+ return input;
148
+ }
149
+ return { src: input };
150
+ }
151
+ function resolveOptions(opts) {
152
+ return {
153
+ src: opts.src,
154
+ wrap: opts.wrap ?? "clamp",
155
+ filter: opts.filter ?? "mipmap",
156
+ vflip: opts.vflip ?? true
157
+ };
131
158
  }
132
159
  function initTexture(gl, unit) {
133
160
  const texture = gl.createTexture();
@@ -139,12 +166,32 @@ function initTexture(gl, unit) {
139
166
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
140
167
  return texture;
141
168
  }
169
+ function applyTextureParameters(gl, w, h, wrap, filter, vflip) {
170
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, vflip ? 1 : 0);
171
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
172
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
173
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
174
+ if (filter === "mipmap") {
175
+ gl.generateMipmap(gl.TEXTURE_2D);
176
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
177
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
178
+ } else if (filter === "nearest") {
179
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
180
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
181
+ } else {
182
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
183
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
184
+ }
185
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
186
+ }
142
187
  function uploadElement(gl, texture, unit, el) {
143
188
  gl.activeTexture(gl.TEXTURE0 + unit);
144
189
  gl.bindTexture(gl.TEXTURE_2D, texture);
145
190
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
146
191
  }
147
- function createTexture(gl, source, unit) {
192
+ function createTexture(gl, input, unit) {
193
+ const opts = resolveOptions(normalizeTextureInput(input));
194
+ const source = opts.src;
148
195
  const texture = initTexture(gl, unit);
149
196
  if (typeof source === "string") {
150
197
  gl.texImage2D(
@@ -176,10 +223,7 @@ function createTexture(gl, source, unit) {
176
223
  return;
177
224
  }
178
225
  uploadElement(gl, texture, unit, img);
179
- if (isPOT(img.width) && isPOT(img.height)) {
180
- gl.generateMipmap(gl.TEXTURE_2D);
181
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
182
- }
226
+ applyTextureParameters(gl, img.width, img.height, opts.wrap, opts.filter, opts.vflip);
183
227
  state2.width = img.width;
184
228
  state2.height = img.height;
185
229
  state2.loaded = true;
@@ -202,6 +246,7 @@ function createTexture(gl, source, unit) {
202
246
  };
203
247
  if (source.complete && source.naturalWidth > 0) {
204
248
  uploadElement(gl, texture, unit, source);
249
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
205
250
  state2.width = source.naturalWidth;
206
251
  state2.height = source.naturalHeight;
207
252
  return { state: state2, promise: null };
@@ -213,6 +258,7 @@ function createTexture(gl, source, unit) {
213
258
  return;
214
259
  }
215
260
  uploadElement(gl, texture, unit, source);
261
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
216
262
  state2.width = source.naturalWidth;
217
263
  state2.height = source.naturalHeight;
218
264
  state2.loaded = true;
@@ -227,6 +273,7 @@ function createTexture(gl, source, unit) {
227
273
  const h = source.videoHeight || 1;
228
274
  if (source.readyState >= 2) {
229
275
  uploadElement(gl, texture, unit, source);
276
+ applyTextureParameters(gl, w, h, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
230
277
  } else {
231
278
  gl.texImage2D(
232
279
  gl.TEXTURE_2D,
@@ -252,6 +299,7 @@ function createTexture(gl, source, unit) {
252
299
  return { state: state2, promise: null };
253
300
  }
254
301
  uploadElement(gl, texture, unit, source);
302
+ applyTextureParameters(gl, source.width, source.height, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
255
303
  const state = {
256
304
  texture,
257
305
  width: source.width,
@@ -298,62 +346,210 @@ function disposeTextures(gl, textures) {
298
346
  }
299
347
 
300
348
  // src/uniforms.ts
301
- function updateUniforms(state, delta, speed, mouse) {
302
- const { gl, locations } = state;
303
- state.time += delta * speed;
304
- if (locations.iTime) {
305
- gl.uniform1f(locations.iTime, state.time);
349
+ function setUniforms(gl, locations, time, delta, frame, width, height, mouse, channelRes) {
350
+ if (locations.iTime) gl.uniform1f(locations.iTime, time);
351
+ if (locations.iTimeDelta) gl.uniform1f(locations.iTimeDelta, delta);
352
+ if (locations.iFrame) gl.uniform1i(locations.iFrame, frame);
353
+ if (locations.iResolution) gl.uniform3f(locations.iResolution, width, height, 1);
354
+ if (locations.iMouse) {
355
+ const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
356
+ const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
357
+ gl.uniform4f(locations.iMouse, mouse.x, mouse.y, mz, mw);
306
358
  }
307
- if (locations.iTimeDelta) {
308
- gl.uniform1f(locations.iTimeDelta, delta);
359
+ if (locations.iChannelResolution && channelRes) {
360
+ gl.uniform3fv(locations.iChannelResolution, channelRes);
309
361
  }
362
+ if (locations.iDate) {
363
+ const now = /* @__PURE__ */ new Date();
364
+ const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
365
+ gl.uniform4f(locations.iDate, now.getFullYear(), now.getMonth(), now.getDate(), seconds);
366
+ }
367
+ }
368
+ function updateUniforms(state, delta, speed, mouse) {
369
+ state.time += delta * speed;
310
370
  state.frame++;
311
- if (locations.iFrame) {
312
- gl.uniform1i(locations.iFrame, state.frame);
371
+ const res = new Float32Array(12);
372
+ for (let i = 0; i < 4; i++) {
373
+ const tex = state.textures[i];
374
+ if (tex) {
375
+ res[i * 3] = tex.width;
376
+ res[i * 3 + 1] = tex.height;
377
+ res[i * 3 + 2] = 1;
378
+ }
313
379
  }
314
- if (locations.iResolution) {
315
- gl.uniform3f(
316
- locations.iResolution,
317
- gl.drawingBufferWidth,
318
- gl.drawingBufferHeight,
319
- 1
320
- );
380
+ setUniforms(
381
+ state.gl,
382
+ state.locations,
383
+ state.time,
384
+ delta,
385
+ state.frame,
386
+ state.gl.drawingBufferWidth,
387
+ state.gl.drawingBufferHeight,
388
+ mouse,
389
+ res
390
+ );
391
+ }
392
+
393
+ // src/multipass.ts
394
+ var PASS_ORDER = ["BufferA", "BufferB", "BufferC", "BufferD", "Image"];
395
+ var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
396
+ function isPassName(v) {
397
+ return typeof v === "string" && PASS_ORDER.includes(v);
398
+ }
399
+ function createPingPongTextures(gl, w, h) {
400
+ const textures = [];
401
+ for (let i = 0; i < 2; i++) {
402
+ const tex = gl.createTexture();
403
+ gl.bindTexture(gl.TEXTURE_2D, tex);
404
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
405
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
406
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
407
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
408
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
409
+ textures.push(tex);
321
410
  }
322
- if (locations.iMouse) {
323
- const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
324
- const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
325
- gl.uniform4f(locations.iMouse, mouse.x, mouse.y, mz, mw);
411
+ return textures;
412
+ }
413
+ function createFBO(gl, texture) {
414
+ const fbo = gl.createFramebuffer();
415
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
416
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
417
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
418
+ return fbo;
419
+ }
420
+ function createMultipassRenderer(gl, config, externalTextures) {
421
+ const w = gl.drawingBufferWidth || 1;
422
+ const h = gl.drawingBufferHeight || 1;
423
+ const passes = [];
424
+ for (const name of PASS_ORDER) {
425
+ const passConfig = config[name];
426
+ if (!passConfig) continue;
427
+ const program = createProgram(gl, passConfig.code);
428
+ if (typeof program === "string") return `${name}: ${program}`;
429
+ setupQuad(gl, program);
430
+ const locations = getUniformLocations(gl, program);
431
+ const isImage = name === "Image";
432
+ const pingPong = isImage ? null : createPingPongTextures(gl, w, h);
433
+ const fbo = isImage ? null : createFBO(gl, pingPong[0]);
434
+ const channelBindings = [null, null, null, null];
435
+ for (let i = 0; i < 4; i++) {
436
+ const input = passConfig[CHANNEL_KEYS[i]];
437
+ if (input == null) continue;
438
+ if (isPassName(input)) {
439
+ channelBindings[i] = { passRef: input };
440
+ } else {
441
+ channelBindings[i] = externalTextures[i];
442
+ }
443
+ }
444
+ passes.push({
445
+ name,
446
+ program,
447
+ locations,
448
+ fbo,
449
+ pingPong,
450
+ currentIdx: 0,
451
+ width: w,
452
+ height: h,
453
+ channelBindings
454
+ });
326
455
  }
327
- if (locations.iChannelResolution) {
328
- const res = new Float32Array(12);
456
+ if (passes.length === 0) return "No passes defined";
457
+ return passes;
458
+ }
459
+ function renderMultipass(gl, passes, delta, speed, mouse, sharedState) {
460
+ sharedState.time += delta * speed;
461
+ sharedState.frame++;
462
+ const passTextures = {};
463
+ for (const pass of passes) {
464
+ const isImage = pass.name === "Image";
465
+ if (pass.pingPong) {
466
+ const writeIdx = pass.currentIdx;
467
+ const readIdx = 1 - writeIdx;
468
+ gl.bindFramebuffer(gl.FRAMEBUFFER, pass.fbo);
469
+ gl.framebufferTexture2D(
470
+ gl.FRAMEBUFFER,
471
+ gl.COLOR_ATTACHMENT0,
472
+ gl.TEXTURE_2D,
473
+ pass.pingPong[writeIdx],
474
+ 0
475
+ );
476
+ passTextures[pass.name] = pass.pingPong[readIdx];
477
+ }
478
+ gl.bindFramebuffer(gl.FRAMEBUFFER, isImage ? null : pass.fbo);
479
+ gl.viewport(0, 0, isImage ? gl.drawingBufferWidth : pass.width, isImage ? gl.drawingBufferHeight : pass.height);
480
+ gl.useProgram(pass.program);
481
+ const tempTextures = [null, null, null, null];
482
+ for (let i = 0; i < 4; i++) {
483
+ const binding = pass.channelBindings[i];
484
+ if (!binding) continue;
485
+ if ("passRef" in binding) {
486
+ const refTex = passTextures[binding.passRef];
487
+ if (refTex) {
488
+ gl.activeTexture(gl.TEXTURE0 + i);
489
+ gl.bindTexture(gl.TEXTURE_2D, refTex);
490
+ if (pass.locations.iChannel[i]) {
491
+ gl.uniform1i(pass.locations.iChannel[i], i);
492
+ }
493
+ }
494
+ } else {
495
+ tempTextures[i] = binding;
496
+ }
497
+ }
498
+ bindTextures(gl, pass.locations.iChannel, tempTextures);
499
+ const channelRes = new Float32Array(12);
329
500
  for (let i = 0; i < 4; i++) {
330
- const tex = state.textures[i];
331
- if (tex) {
332
- res[i * 3] = tex.width;
333
- res[i * 3 + 1] = tex.height;
334
- res[i * 3 + 2] = 1;
501
+ const binding = pass.channelBindings[i];
502
+ if (!binding) continue;
503
+ if ("passRef" in binding) {
504
+ const refPass = passes.find((p) => p.name === binding.passRef);
505
+ if (refPass) {
506
+ channelRes[i * 3] = refPass.width;
507
+ channelRes[i * 3 + 1] = refPass.height;
508
+ channelRes[i * 3 + 2] = 1;
509
+ }
510
+ } else {
511
+ channelRes[i * 3] = binding.width;
512
+ channelRes[i * 3 + 1] = binding.height;
513
+ channelRes[i * 3 + 2] = 1;
335
514
  }
336
515
  }
337
- gl.uniform3fv(locations.iChannelResolution, res);
516
+ const vw = isImage ? gl.drawingBufferWidth : pass.width;
517
+ const vh = isImage ? gl.drawingBufferHeight : pass.height;
518
+ setUniforms(gl, pass.locations, sharedState.time, delta, sharedState.frame, vw, vh, mouse, channelRes);
519
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
520
+ if (pass.pingPong) {
521
+ pass.currentIdx = 1 - pass.currentIdx;
522
+ }
338
523
  }
339
- if (locations.iDate) {
340
- const now = /* @__PURE__ */ new Date();
341
- const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
342
- gl.uniform4f(
343
- locations.iDate,
344
- now.getFullYear(),
345
- now.getMonth(),
346
- // 0-based, matches Shadertoy
347
- now.getDate(),
348
- seconds
349
- );
524
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
525
+ }
526
+ function resizeFBOs(gl, passes, w, h) {
527
+ for (const pass of passes) {
528
+ if (!pass.pingPong) continue;
529
+ pass.width = w;
530
+ pass.height = h;
531
+ for (let i = 0; i < 2; i++) {
532
+ gl.bindTexture(gl.TEXTURE_2D, pass.pingPong[i]);
533
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
534
+ }
535
+ }
536
+ }
537
+ function disposeMultipass(gl, passes) {
538
+ for (const pass of passes) {
539
+ gl.deleteProgram(pass.program);
540
+ if (pass.fbo) gl.deleteFramebuffer(pass.fbo);
541
+ if (pass.pingPong) {
542
+ gl.deleteTexture(pass.pingPong[0]);
543
+ gl.deleteTexture(pass.pingPong[1]);
544
+ }
350
545
  }
351
546
  }
352
547
 
353
548
  // src/useShadertoy.ts
354
- var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
549
+ var CHANNEL_KEYS2 = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
355
550
  function useShadertoy({
356
551
  fragmentShader,
552
+ passes: passesProp,
357
553
  textures: texturesProp,
358
554
  paused = false,
359
555
  speed = 1,
@@ -364,6 +560,7 @@ function useShadertoy({
364
560
  }) {
365
561
  const canvasRef = useRef(null);
366
562
  const rendererRef = useRef(null);
563
+ const multipassRef = useRef(null);
367
564
  const rafRef = useRef(0);
368
565
  const pausedRef = useRef(paused);
369
566
  const speedRef = useRef(speed);
@@ -376,25 +573,33 @@ function useShadertoy({
376
573
  clickY: 0,
377
574
  pressed: false
378
575
  });
576
+ const sharedState = useRef({ time: 0, frame: 0 });
379
577
  pausedRef.current = paused;
380
578
  speedRef.current = speed;
579
+ const isMultipass = !!passesProp;
381
580
  useEffect(() => {
382
581
  const canvas = canvasRef.current;
383
582
  if (!canvas) return;
384
- const result = createRenderer(canvas, fragmentShader);
385
- if (typeof result === "string") {
386
- setError(result);
387
- onError?.(result);
583
+ sharedState.current = { time: 0, frame: 0 };
584
+ const gl = canvas.getContext("webgl2", {
585
+ antialias: false,
586
+ alpha: true,
587
+ premultipliedAlpha: false
588
+ });
589
+ if (!gl) {
590
+ const msg = "WebGL2 not supported";
591
+ setError(msg);
592
+ onError?.(msg);
388
593
  return;
389
594
  }
390
- rendererRef.current = result;
595
+ const externalTextures = [null, null, null, null];
391
596
  const texturePromises = [];
392
597
  if (texturesProp) {
393
598
  for (let i = 0; i < 4; i++) {
394
- const src = texturesProp[CHANNEL_KEYS[i]];
599
+ const src = texturesProp[CHANNEL_KEYS2[i]];
395
600
  if (src != null) {
396
- const { state, promise } = createTexture(result.gl, src, i);
397
- result.textures[i] = state;
601
+ const { state, promise } = createTexture(gl, src, i);
602
+ externalTextures[i] = state;
398
603
  if (promise) texturePromises.push(promise);
399
604
  }
400
605
  }
@@ -404,41 +609,88 @@ function useShadertoy({
404
609
  setError(null);
405
610
  onLoad?.();
406
611
  };
407
- if (texturePromises.length > 0) {
408
- Promise.all(texturePromises).then(() => {
409
- if (rendererRef.current) markReady();
410
- }).catch((err) => {
411
- const msg = err instanceof Error ? err.message : "Texture load failed";
412
- setError(msg);
413
- onError?.(msg);
414
- });
415
- } else {
416
- markReady();
417
- }
418
- let lastTimestamp = 0;
419
- const loop = (timestamp) => {
420
- const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
421
- lastTimestamp = timestamp;
422
- if (!pausedRef.current && rendererRef.current) {
423
- const r = rendererRef.current;
424
- updateDynamicTextures(r.gl, r.textures);
425
- bindTextures(r.gl, r.locations.iChannel, r.textures);
426
- updateUniforms(r, delta, speedRef.current, mouseState.current);
427
- render(r);
612
+ const handleError = (msg) => {
613
+ setError(msg);
614
+ onError?.(msg);
615
+ };
616
+ if (isMultipass) {
617
+ const passResult = createMultipassRenderer(gl, passesProp, externalTextures);
618
+ if (typeof passResult === "string") {
619
+ handleError(passResult);
620
+ return;
428
621
  }
622
+ multipassRef.current = passResult;
623
+ rendererRef.current = null;
624
+ if (texturePromises.length > 0) {
625
+ Promise.all(texturePromises).then(() => {
626
+ if (multipassRef.current) markReady();
627
+ }).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
628
+ } else {
629
+ markReady();
630
+ }
631
+ let lastTimestamp = 0;
632
+ const loop = (timestamp) => {
633
+ const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
634
+ lastTimestamp = timestamp;
635
+ if (!pausedRef.current && multipassRef.current) {
636
+ updateDynamicTextures(gl, externalTextures);
637
+ renderMultipass(gl, multipassRef.current, delta, speedRef.current, mouseState.current, sharedState.current);
638
+ }
639
+ rafRef.current = requestAnimationFrame(loop);
640
+ };
429
641
  rafRef.current = requestAnimationFrame(loop);
430
- };
431
- rafRef.current = requestAnimationFrame(loop);
432
- return () => {
433
- cancelAnimationFrame(rafRef.current);
434
- if (rendererRef.current) {
435
- disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
436
- dispose(rendererRef.current);
437
- rendererRef.current = null;
642
+ return () => {
643
+ cancelAnimationFrame(rafRef.current);
644
+ if (multipassRef.current) {
645
+ disposeMultipass(gl, multipassRef.current);
646
+ multipassRef.current = null;
647
+ }
648
+ disposeTextures(gl, externalTextures);
649
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
650
+ setIsReady(false);
651
+ };
652
+ } else {
653
+ const shaderCode = fragmentShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
654
+ const result = createRenderer(canvas, shaderCode);
655
+ if (typeof result === "string") {
656
+ handleError(result);
657
+ return;
438
658
  }
439
- setIsReady(false);
440
- };
441
- }, [fragmentShader, texturesProp, onError, onLoad]);
659
+ rendererRef.current = result;
660
+ multipassRef.current = null;
661
+ result.textures = externalTextures;
662
+ if (texturePromises.length > 0) {
663
+ Promise.all(texturePromises).then(() => {
664
+ if (rendererRef.current) markReady();
665
+ }).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
666
+ } else {
667
+ markReady();
668
+ }
669
+ let lastTimestamp = 0;
670
+ const loop = (timestamp) => {
671
+ const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
672
+ lastTimestamp = timestamp;
673
+ if (!pausedRef.current && rendererRef.current) {
674
+ const r = rendererRef.current;
675
+ updateDynamicTextures(r.gl, r.textures);
676
+ bindTextures(r.gl, r.locations.iChannel, r.textures);
677
+ updateUniforms(r, delta, speedRef.current, mouseState.current);
678
+ render(r);
679
+ }
680
+ rafRef.current = requestAnimationFrame(loop);
681
+ };
682
+ rafRef.current = requestAnimationFrame(loop);
683
+ return () => {
684
+ cancelAnimationFrame(rafRef.current);
685
+ if (rendererRef.current) {
686
+ disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
687
+ dispose(rendererRef.current);
688
+ rendererRef.current = null;
689
+ }
690
+ setIsReady(false);
691
+ };
692
+ }
693
+ }, [fragmentShader, passesProp, texturesProp, onError, onLoad]);
442
694
  useEffect(() => {
443
695
  const canvas = canvasRef.current;
444
696
  if (!canvas) return;
@@ -446,8 +698,14 @@ function useShadertoy({
446
698
  const observer = new ResizeObserver((entries) => {
447
699
  for (const entry of entries) {
448
700
  const { width, height } = entry.contentRect;
449
- canvas.width = Math.round(width * dpr);
450
- canvas.height = Math.round(height * dpr);
701
+ const w = Math.round(width * dpr);
702
+ const h = Math.round(height * dpr);
703
+ canvas.width = w;
704
+ canvas.height = h;
705
+ if (multipassRef.current) {
706
+ const gl = canvas.getContext("webgl2");
707
+ if (gl) resizeFBOs(gl, multipassRef.current, w, h);
708
+ }
451
709
  }
452
710
  });
453
711
  observer.observe(canvas);
@@ -521,6 +779,7 @@ function useShadertoy({
521
779
  import { jsx } from "react/jsx-runtime";
522
780
  function Shadertoy({
523
781
  fragmentShader,
782
+ passes,
524
783
  textures,
525
784
  style,
526
785
  className,
@@ -533,6 +792,7 @@ function Shadertoy({
533
792
  }) {
534
793
  const { canvasRef } = useShadertoy({
535
794
  fragmentShader,
795
+ passes,
536
796
  textures,
537
797
  paused,
538
798
  speed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-shadertoy",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Run Shadertoy GLSL shaders in React. Copy-paste and it works.",
5
5
  "author": "Wrennly (https://github.com/wrennly)",
6
6
  "license": "MIT",