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/README.md +2 -1
- package/dist/index.d.mts +39 -10
- package/dist/index.d.ts +39 -10
- package/dist/index.js +371 -111
- package/dist/index.mjs +371 -111
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
41
|
-
#define texture
|
|
42
|
+
// Shadertoy compat: older shaders may use texture2D()
|
|
43
|
+
#define texture2D texture
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
`;
|
|
46
|
+
function wrapFragmentShader(shader) {
|
|
47
|
+
return FRAGMENT_PREAMBLE + shader + `
|
|
44
48
|
|
|
45
49
|
void main() {
|
|
46
|
-
mainImage(
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
130
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (locations.
|
|
305
|
-
|
|
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.
|
|
308
|
-
gl.
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
gl
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 (
|
|
328
|
-
|
|
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
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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.
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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[
|
|
599
|
+
const src = texturesProp[CHANNEL_KEYS2[i]];
|
|
395
600
|
if (src != null) {
|
|
396
|
-
const { state, promise } = createTexture(
|
|
397
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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,
|