react-shadertoy 0.2.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,50 +169,191 @@ function dispose(state) {
153
169
  }
154
170
 
155
171
  // src/textures.ts
156
- function loadImageTexture(gl, url, unit) {
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
+ };
185
+ }
186
+ function initTexture(gl, unit) {
157
187
  const texture = gl.createTexture();
158
188
  gl.activeTexture(gl.TEXTURE0 + unit);
159
189
  gl.bindTexture(gl.TEXTURE_2D, texture);
160
- gl.texImage2D(
161
- gl.TEXTURE_2D,
162
- 0,
163
- gl.RGBA,
164
- 1,
165
- 1,
166
- 0,
167
- gl.RGBA,
168
- gl.UNSIGNED_BYTE,
169
- new Uint8Array([255, 0, 255, 255])
170
- );
171
190
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
172
191
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
173
192
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
174
193
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
175
- const state = { texture, width: 1, height: 1, unit, loaded: false };
176
- const promise = new Promise((resolve, reject) => {
177
- const img = new Image();
178
- img.crossOrigin = "anonymous";
179
- img.onload = () => {
180
- if (gl.isContextLost()) {
194
+ return texture;
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
+ }
214
+ function uploadElement(gl, texture, unit, el) {
215
+ gl.activeTexture(gl.TEXTURE0 + unit);
216
+ gl.bindTexture(gl.TEXTURE_2D, texture);
217
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
218
+ }
219
+ function createTexture(gl, input, unit) {
220
+ const opts = resolveOptions(normalizeTextureInput(input));
221
+ const source = opts.src;
222
+ const texture = initTexture(gl, unit);
223
+ if (typeof source === "string") {
224
+ gl.texImage2D(
225
+ gl.TEXTURE_2D,
226
+ 0,
227
+ gl.RGBA,
228
+ 1,
229
+ 1,
230
+ 0,
231
+ gl.RGBA,
232
+ gl.UNSIGNED_BYTE,
233
+ new Uint8Array([255, 0, 255, 255])
234
+ );
235
+ const state2 = {
236
+ texture,
237
+ width: 1,
238
+ height: 1,
239
+ unit,
240
+ loaded: false,
241
+ needsUpdate: false,
242
+ source
243
+ };
244
+ const promise = new Promise((resolve, reject) => {
245
+ const img = new Image();
246
+ img.crossOrigin = "anonymous";
247
+ img.onload = () => {
248
+ if (gl.isContextLost()) {
249
+ resolve();
250
+ return;
251
+ }
252
+ uploadElement(gl, texture, unit, img);
253
+ applyTextureParameters(gl, img.width, img.height, opts.wrap, opts.filter, opts.vflip);
254
+ state2.width = img.width;
255
+ state2.height = img.height;
256
+ state2.loaded = true;
181
257
  resolve();
182
- return;
183
- }
184
- gl.activeTexture(gl.TEXTURE0 + unit);
185
- gl.bindTexture(gl.TEXTURE_2D, texture);
186
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
187
- if (isPOT(img.width) && isPOT(img.height)) {
188
- gl.generateMipmap(gl.TEXTURE_2D);
189
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
190
- }
191
- state.width = img.width;
192
- state.height = img.height;
193
- state.loaded = true;
194
- resolve();
258
+ };
259
+ img.onerror = () => reject(new Error(`Failed to load texture: ${source}`));
260
+ img.src = source;
261
+ });
262
+ return { state: state2, promise };
263
+ }
264
+ if (source instanceof HTMLImageElement) {
265
+ const state2 = {
266
+ texture,
267
+ width: source.naturalWidth || 1,
268
+ height: source.naturalHeight || 1,
269
+ unit,
270
+ loaded: source.complete,
271
+ needsUpdate: false,
272
+ source
195
273
  };
196
- img.onerror = () => reject(new Error(`Failed to load texture: ${url}`));
197
- img.src = url;
198
- });
199
- return { state, promise };
274
+ if (source.complete && source.naturalWidth > 0) {
275
+ uploadElement(gl, texture, unit, source);
276
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
277
+ state2.width = source.naturalWidth;
278
+ state2.height = source.naturalHeight;
279
+ return { state: state2, promise: null };
280
+ }
281
+ const promise = new Promise((resolve, reject) => {
282
+ source.onload = () => {
283
+ if (gl.isContextLost()) {
284
+ resolve();
285
+ return;
286
+ }
287
+ uploadElement(gl, texture, unit, source);
288
+ applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
289
+ state2.width = source.naturalWidth;
290
+ state2.height = source.naturalHeight;
291
+ state2.loaded = true;
292
+ resolve();
293
+ };
294
+ source.onerror = () => reject(new Error("Failed to load image element"));
295
+ });
296
+ return { state: state2, promise };
297
+ }
298
+ if (source instanceof HTMLVideoElement) {
299
+ const w = source.videoWidth || 1;
300
+ const h = source.videoHeight || 1;
301
+ if (source.readyState >= 2) {
302
+ uploadElement(gl, texture, unit, source);
303
+ applyTextureParameters(gl, w, h, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
304
+ } else {
305
+ gl.texImage2D(
306
+ gl.TEXTURE_2D,
307
+ 0,
308
+ gl.RGBA,
309
+ 1,
310
+ 1,
311
+ 0,
312
+ gl.RGBA,
313
+ gl.UNSIGNED_BYTE,
314
+ new Uint8Array([0, 0, 0, 255])
315
+ );
316
+ }
317
+ const state2 = {
318
+ texture,
319
+ width: w,
320
+ height: h,
321
+ unit,
322
+ loaded: source.readyState >= 2,
323
+ needsUpdate: true,
324
+ source
325
+ };
326
+ return { state: state2, promise: null };
327
+ }
328
+ uploadElement(gl, texture, unit, source);
329
+ applyTextureParameters(gl, source.width, source.height, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
330
+ const state = {
331
+ texture,
332
+ width: source.width,
333
+ height: source.height,
334
+ unit,
335
+ loaded: true,
336
+ needsUpdate: true,
337
+ source
338
+ };
339
+ return { state, promise: null };
340
+ }
341
+ function updateDynamicTextures(gl, textures) {
342
+ for (const tex of textures) {
343
+ if (!tex || !tex.needsUpdate || !tex.source) continue;
344
+ if (tex.source instanceof HTMLVideoElement) {
345
+ const v = tex.source;
346
+ if (v.readyState < 2) continue;
347
+ uploadElement(gl, tex.texture, tex.unit, v);
348
+ tex.width = v.videoWidth;
349
+ tex.height = v.videoHeight;
350
+ tex.loaded = true;
351
+ } else if (tex.source instanceof HTMLCanvasElement) {
352
+ uploadElement(gl, tex.texture, tex.unit, tex.source);
353
+ tex.width = tex.source.width;
354
+ tex.height = tex.source.height;
355
+ }
356
+ }
200
357
  }
201
358
  function bindTextures(gl, locations, textures) {
202
359
  for (let i = 0; i < 4; i++) {
@@ -214,67 +371,212 @@ function disposeTextures(gl, textures) {
214
371
  if (tex) gl.deleteTexture(tex.texture);
215
372
  }
216
373
  }
217
- function isPOT(v) {
218
- return (v & v - 1) === 0 && v > 0;
219
- }
220
374
 
221
375
  // src/uniforms.ts
222
- function updateUniforms(state, delta, speed, mouse) {
223
- const { gl, locations } = state;
224
- state.time += delta * speed;
225
- if (locations.iTime) {
226
- 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);
227
385
  }
228
- if (locations.iTimeDelta) {
229
- gl.uniform1f(locations.iTimeDelta, delta);
386
+ if (locations.iChannelResolution && channelRes) {
387
+ gl.uniform3fv(locations.iChannelResolution, channelRes);
230
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;
231
397
  state.frame++;
232
- if (locations.iFrame) {
233
- 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
+ }
234
406
  }
235
- if (locations.iResolution) {
236
- gl.uniform3f(
237
- locations.iResolution,
238
- gl.drawingBufferWidth,
239
- gl.drawingBufferHeight,
240
- 1
241
- );
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);
242
437
  }
243
- if (locations.iMouse) {
244
- const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
245
- const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
246
- 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
+ });
247
482
  }
248
- if (locations.iChannelResolution) {
249
- 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);
250
527
  for (let i = 0; i < 4; i++) {
251
- const tex = state.textures[i];
252
- if (tex) {
253
- res[i * 3] = tex.width;
254
- res[i * 3 + 1] = tex.height;
255
- 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;
256
541
  }
257
542
  }
258
- 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
+ }
259
550
  }
260
- if (locations.iDate) {
261
- const now = /* @__PURE__ */ new Date();
262
- const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
263
- gl.uniform4f(
264
- locations.iDate,
265
- now.getFullYear(),
266
- now.getMonth(),
267
- // 0-based, matches Shadertoy
268
- now.getDate(),
269
- seconds
270
- );
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
+ }
271
572
  }
272
573
  }
273
574
 
274
575
  // src/useShadertoy.ts
275
- var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
576
+ var CHANNEL_KEYS2 = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
276
577
  function useShadertoy({
277
578
  fragmentShader,
579
+ passes: passesProp,
278
580
  textures: texturesProp,
279
581
  paused = false,
280
582
  speed = 1,
@@ -285,6 +587,7 @@ function useShadertoy({
285
587
  }) {
286
588
  const canvasRef = (0, import_react.useRef)(null);
287
589
  const rendererRef = (0, import_react.useRef)(null);
590
+ const multipassRef = (0, import_react.useRef)(null);
288
591
  const rafRef = (0, import_react.useRef)(0);
289
592
  const pausedRef = (0, import_react.useRef)(paused);
290
593
  const speedRef = (0, import_react.useRef)(speed);
@@ -297,26 +600,34 @@ function useShadertoy({
297
600
  clickY: 0,
298
601
  pressed: false
299
602
  });
603
+ const sharedState = (0, import_react.useRef)({ time: 0, frame: 0 });
300
604
  pausedRef.current = paused;
301
605
  speedRef.current = speed;
606
+ const isMultipass = !!passesProp;
302
607
  (0, import_react.useEffect)(() => {
303
608
  const canvas = canvasRef.current;
304
609
  if (!canvas) return;
305
- const result = createRenderer(canvas, fragmentShader);
306
- if (typeof result === "string") {
307
- setError(result);
308
- 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);
309
620
  return;
310
621
  }
311
- rendererRef.current = result;
622
+ const externalTextures = [null, null, null, null];
312
623
  const texturePromises = [];
313
624
  if (texturesProp) {
314
625
  for (let i = 0; i < 4; i++) {
315
- const src = texturesProp[CHANNEL_KEYS[i]];
316
- if (typeof src === "string") {
317
- const { state, promise } = loadImageTexture(result.gl, src, i);
318
- result.textures[i] = state;
319
- texturePromises.push(promise);
626
+ const src = texturesProp[CHANNEL_KEYS2[i]];
627
+ if (src != null) {
628
+ const { state, promise } = createTexture(gl, src, i);
629
+ externalTextures[i] = state;
630
+ if (promise) texturePromises.push(promise);
320
631
  }
321
632
  }
322
633
  }
@@ -325,40 +636,88 @@ function useShadertoy({
325
636
  setError(null);
326
637
  onLoad?.();
327
638
  };
328
- if (texturePromises.length > 0) {
329
- Promise.all(texturePromises).then(() => {
330
- if (rendererRef.current) markReady();
331
- }).catch((err) => {
332
- const msg = err instanceof Error ? err.message : "Texture load failed";
333
- setError(msg);
334
- onError?.(msg);
335
- });
336
- } else {
337
- markReady();
338
- }
339
- let lastTimestamp = 0;
340
- const loop = (timestamp) => {
341
- const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
342
- lastTimestamp = timestamp;
343
- if (!pausedRef.current && rendererRef.current) {
344
- const r = rendererRef.current;
345
- bindTextures(r.gl, r.locations.iChannel, r.textures);
346
- updateUniforms(r, delta, speedRef.current, mouseState.current);
347
- 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;
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();
348
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
+ };
349
668
  rafRef.current = requestAnimationFrame(loop);
350
- };
351
- rafRef.current = requestAnimationFrame(loop);
352
- return () => {
353
- cancelAnimationFrame(rafRef.current);
354
- if (rendererRef.current) {
355
- disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
356
- dispose(rendererRef.current);
357
- 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;
358
685
  }
359
- setIsReady(false);
360
- };
361
- }, [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]);
362
721
  (0, import_react.useEffect)(() => {
363
722
  const canvas = canvasRef.current;
364
723
  if (!canvas) return;
@@ -366,8 +725,14 @@ function useShadertoy({
366
725
  const observer = new ResizeObserver((entries) => {
367
726
  for (const entry of entries) {
368
727
  const { width, height } = entry.contentRect;
369
- canvas.width = Math.round(width * dpr);
370
- 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
+ }
371
736
  }
372
737
  });
373
738
  observer.observe(canvas);
@@ -441,6 +806,7 @@ function useShadertoy({
441
806
  var import_jsx_runtime = require("react/jsx-runtime");
442
807
  function Shadertoy({
443
808
  fragmentShader,
809
+ passes,
444
810
  textures,
445
811
  style,
446
812
  className,
@@ -453,6 +819,7 @@ function Shadertoy({
453
819
  }) {
454
820
  const { canvasRef } = useShadertoy({
455
821
  fragmentShader,
822
+ passes,
456
823
  textures,
457
824
  paused,
458
825
  speed,