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.js
CHANGED
|
@@ -43,14 +43,16 @@ var QUAD_VERTICES = new Float32Array([
|
|
|
43
43
|
1,
|
|
44
44
|
1
|
|
45
45
|
]);
|
|
46
|
-
var VERTEX_SHADER =
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
68
|
-
#define texture
|
|
69
|
+
// Shadertoy compat: older shaders may use texture2D()
|
|
70
|
+
#define texture2D texture
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
`;
|
|
73
|
+
function wrapFragmentShader(shader) {
|
|
74
|
+
return FRAGMENT_PREAMBLE + shader + `
|
|
71
75
|
|
|
72
76
|
void main() {
|
|
73
|
-
mainImage(
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
157
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (locations.
|
|
332
|
-
|
|
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.
|
|
335
|
-
gl.
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
342
|
-
gl
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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 (
|
|
355
|
-
|
|
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
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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[
|
|
626
|
+
const src = texturesProp[CHANNEL_KEYS2[i]];
|
|
422
627
|
if (src != null) {
|
|
423
|
-
const { state, promise } = createTexture(
|
|
424
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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,
|