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/README.md +2 -1
- package/dist/index.d.mts +41 -12
- package/dist/index.d.ts +41 -12
- package/dist/index.js +510 -143
- package/dist/index.mjs +510 -143
- 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,50 +169,191 @@ function dispose(state) {
|
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
// src/textures.ts
|
|
156
|
-
function
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (locations.
|
|
226
|
-
|
|
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.
|
|
229
|
-
gl.
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
gl
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 (
|
|
249
|
-
|
|
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
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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[
|
|
316
|
-
if (
|
|
317
|
-
const { state, promise } =
|
|
318
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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,
|