react-shadertoy 0.3.0 → 0.6.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 +101 -5
- package/dist/index.d.mts +115 -10
- package/dist/index.d.ts +115 -10
- package/dist/index.js +543 -114
- package/dist/index.mjs +542 -115
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Shadertoy: () => Shadertoy,
|
|
24
|
+
apiToConfig: () => apiToConfig,
|
|
25
|
+
fetchShader: () => fetchShader,
|
|
24
26
|
useShadertoy: () => useShadertoy
|
|
25
27
|
});
|
|
26
28
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -28,6 +30,98 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
30
|
// src/useShadertoy.ts
|
|
29
31
|
var import_react = require("react");
|
|
30
32
|
|
|
33
|
+
// src/api.ts
|
|
34
|
+
var SHADERTOY_BASE = "https://www.shadertoy.com";
|
|
35
|
+
var API_URL = `${SHADERTOY_BASE}/api/v1/shaders`;
|
|
36
|
+
var OUTPUT_ID_TO_PASS = {
|
|
37
|
+
257: "BufferA",
|
|
38
|
+
258: "BufferB",
|
|
39
|
+
259: "BufferC",
|
|
40
|
+
260: "BufferD"
|
|
41
|
+
};
|
|
42
|
+
var cache = /* @__PURE__ */ new Map();
|
|
43
|
+
async function fetchShader(id, apiKey) {
|
|
44
|
+
const cached = cache.get(id);
|
|
45
|
+
if (cached) return cached;
|
|
46
|
+
const res = await fetch(`${API_URL}/${id}?key=${apiKey}`);
|
|
47
|
+
if (!res.ok) throw new Error(`Shadertoy API error: ${res.status}`);
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
if (data.Error) throw new Error(`Shadertoy API: ${data.Error}`);
|
|
50
|
+
cache.set(id, data.Shader);
|
|
51
|
+
return data.Shader;
|
|
52
|
+
}
|
|
53
|
+
function mapWrap(wrap) {
|
|
54
|
+
if (wrap === "repeat") return "repeat";
|
|
55
|
+
return "clamp";
|
|
56
|
+
}
|
|
57
|
+
function mapFilter(filter) {
|
|
58
|
+
if (filter === "nearest") return "nearest";
|
|
59
|
+
if (filter === "linear") return "linear";
|
|
60
|
+
return "mipmap";
|
|
61
|
+
}
|
|
62
|
+
function resolveTextureSrc(src) {
|
|
63
|
+
if (src.startsWith("http")) return src;
|
|
64
|
+
return SHADERTOY_BASE + src;
|
|
65
|
+
}
|
|
66
|
+
function apiToConfig(shader) {
|
|
67
|
+
const passes = {};
|
|
68
|
+
const textures = {};
|
|
69
|
+
const commonPass = shader.renderpass.find((p) => p.type === "common");
|
|
70
|
+
const commonCode = commonPass ? commonPass.code + "\n" : "";
|
|
71
|
+
for (const rp of shader.renderpass) {
|
|
72
|
+
if (rp.type === "common" || rp.type === "sound") continue;
|
|
73
|
+
const passName = getPassName(rp);
|
|
74
|
+
if (!passName) continue;
|
|
75
|
+
const passConfig = {
|
|
76
|
+
code: commonCode + rp.code
|
|
77
|
+
};
|
|
78
|
+
for (const input of rp.inputs) {
|
|
79
|
+
const channelKey = `iChannel${input.channel}`;
|
|
80
|
+
if (channelKey === "code") continue;
|
|
81
|
+
if (input.ctype === "buffer") {
|
|
82
|
+
const refPass = OUTPUT_ID_TO_PASS[input.id];
|
|
83
|
+
if (refPass) {
|
|
84
|
+
;
|
|
85
|
+
passConfig[channelKey] = refPass;
|
|
86
|
+
}
|
|
87
|
+
} else if (input.ctype === "texture" || input.ctype === "cubemap") {
|
|
88
|
+
const texOpts = {
|
|
89
|
+
src: resolveTextureSrc(input.src),
|
|
90
|
+
wrap: mapWrap(input.sampler.wrap),
|
|
91
|
+
filter: mapFilter(input.sampler.filter),
|
|
92
|
+
vflip: input.sampler.vflip === "true"
|
|
93
|
+
};
|
|
94
|
+
passConfig[channelKey] = texOpts;
|
|
95
|
+
const texKey = `iChannel${input.channel}`;
|
|
96
|
+
textures[texKey] = texOpts;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
passes[passName] = passConfig;
|
|
100
|
+
}
|
|
101
|
+
const meta = {
|
|
102
|
+
name: shader.info.name,
|
|
103
|
+
author: shader.info.username,
|
|
104
|
+
description: shader.info.description,
|
|
105
|
+
tags: shader.info.tags
|
|
106
|
+
};
|
|
107
|
+
return { passes, textures, meta };
|
|
108
|
+
}
|
|
109
|
+
function getPassName(rp) {
|
|
110
|
+
if (rp.type === "image") return "Image";
|
|
111
|
+
if (rp.type === "buffer") {
|
|
112
|
+
for (const out of rp.outputs) {
|
|
113
|
+
const name = OUTPUT_ID_TO_PASS[out.id];
|
|
114
|
+
if (name) return name;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function isSinglePass(passes) {
|
|
121
|
+
const keys = Object.keys(passes);
|
|
122
|
+
return keys.length === 1 && keys[0] === "Image";
|
|
123
|
+
}
|
|
124
|
+
|
|
31
125
|
// src/renderer.ts
|
|
32
126
|
var QUAD_VERTICES = new Float32Array([
|
|
33
127
|
-1,
|
|
@@ -43,14 +137,16 @@ var QUAD_VERTICES = new Float32Array([
|
|
|
43
137
|
1,
|
|
44
138
|
1
|
|
45
139
|
]);
|
|
46
|
-
var VERTEX_SHADER =
|
|
47
|
-
|
|
140
|
+
var VERTEX_SHADER = `#version 300 es
|
|
141
|
+
in vec2 position;
|
|
48
142
|
void main() {
|
|
49
143
|
gl_Position = vec4(position, 0.0, 1.0);
|
|
50
144
|
}
|
|
51
145
|
`;
|
|
52
|
-
|
|
53
|
-
|
|
146
|
+
var FRAGMENT_PREAMBLE = `#version 300 es
|
|
147
|
+
precision highp float;
|
|
148
|
+
|
|
149
|
+
out vec4 _fragColor;
|
|
54
150
|
|
|
55
151
|
uniform vec3 iResolution;
|
|
56
152
|
uniform float iTime;
|
|
@@ -64,13 +160,16 @@ uniform sampler2D iChannel2;
|
|
|
64
160
|
uniform sampler2D iChannel3;
|
|
65
161
|
uniform vec3 iChannelResolution[4];
|
|
66
162
|
|
|
67
|
-
// Shadertoy
|
|
68
|
-
#define texture
|
|
163
|
+
// Shadertoy compat: older shaders may use texture2D()
|
|
164
|
+
#define texture2D texture
|
|
69
165
|
|
|
70
|
-
|
|
166
|
+
`;
|
|
167
|
+
function wrapFragmentShader(shader) {
|
|
168
|
+
return FRAGMENT_PREAMBLE + shader + `
|
|
71
169
|
|
|
72
170
|
void main() {
|
|
73
|
-
mainImage(
|
|
171
|
+
mainImage(_fragColor, gl_FragCoord.xy);
|
|
172
|
+
_fragColor.a = 1.0;
|
|
74
173
|
}
|
|
75
174
|
`;
|
|
76
175
|
}
|
|
@@ -86,13 +185,7 @@ function compileShader(gl, type, source) {
|
|
|
86
185
|
}
|
|
87
186
|
return shader;
|
|
88
187
|
}
|
|
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";
|
|
188
|
+
function createProgram(gl, fragmentShader) {
|
|
96
189
|
const vert = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
97
190
|
if (typeof vert === "string") return vert;
|
|
98
191
|
const frag = compileShader(gl, gl.FRAGMENT_SHADER, wrapFragmentShader(fragmentShader));
|
|
@@ -109,13 +202,10 @@ function createRenderer(canvas, fragmentShader) {
|
|
|
109
202
|
}
|
|
110
203
|
gl.deleteShader(vert);
|
|
111
204
|
gl.deleteShader(frag);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
gl.enableVertexAttribArray(positionLoc);
|
|
117
|
-
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
|
|
118
|
-
const locations = {
|
|
205
|
+
return program;
|
|
206
|
+
}
|
|
207
|
+
function getUniformLocations(gl, program) {
|
|
208
|
+
return {
|
|
119
209
|
iResolution: gl.getUniformLocation(program, "iResolution"),
|
|
120
210
|
iTime: gl.getUniformLocation(program, "iTime"),
|
|
121
211
|
iTimeDelta: gl.getUniformLocation(program, "iTimeDelta"),
|
|
@@ -130,6 +220,26 @@ function createRenderer(canvas, fragmentShader) {
|
|
|
130
220
|
],
|
|
131
221
|
iChannelResolution: gl.getUniformLocation(program, "iChannelResolution")
|
|
132
222
|
};
|
|
223
|
+
}
|
|
224
|
+
function setupQuad(gl, program) {
|
|
225
|
+
const buffer = gl.createBuffer();
|
|
226
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
227
|
+
gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
|
|
228
|
+
const positionLoc = gl.getAttribLocation(program, "position");
|
|
229
|
+
gl.enableVertexAttribArray(positionLoc);
|
|
230
|
+
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
|
|
231
|
+
}
|
|
232
|
+
function createRenderer(canvas, fragmentShader) {
|
|
233
|
+
const gl = canvas.getContext("webgl2", {
|
|
234
|
+
antialias: false,
|
|
235
|
+
alpha: true,
|
|
236
|
+
premultipliedAlpha: false
|
|
237
|
+
});
|
|
238
|
+
if (!gl) return "WebGL2 not supported";
|
|
239
|
+
const program = createProgram(gl, fragmentShader);
|
|
240
|
+
if (typeof program === "string") return program;
|
|
241
|
+
setupQuad(gl, program);
|
|
242
|
+
const locations = getUniformLocations(gl, program);
|
|
133
243
|
gl.useProgram(program);
|
|
134
244
|
return {
|
|
135
245
|
gl,
|
|
@@ -153,8 +263,19 @@ function dispose(state) {
|
|
|
153
263
|
}
|
|
154
264
|
|
|
155
265
|
// src/textures.ts
|
|
156
|
-
function
|
|
157
|
-
|
|
266
|
+
function normalizeTextureInput(input) {
|
|
267
|
+
if (typeof input === "object" && input !== null && "src" in input) {
|
|
268
|
+
return input;
|
|
269
|
+
}
|
|
270
|
+
return { src: input };
|
|
271
|
+
}
|
|
272
|
+
function resolveOptions(opts) {
|
|
273
|
+
return {
|
|
274
|
+
src: opts.src,
|
|
275
|
+
wrap: opts.wrap ?? "clamp",
|
|
276
|
+
filter: opts.filter ?? "mipmap",
|
|
277
|
+
vflip: opts.vflip ?? true
|
|
278
|
+
};
|
|
158
279
|
}
|
|
159
280
|
function initTexture(gl, unit) {
|
|
160
281
|
const texture = gl.createTexture();
|
|
@@ -166,12 +287,32 @@ function initTexture(gl, unit) {
|
|
|
166
287
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
167
288
|
return texture;
|
|
168
289
|
}
|
|
290
|
+
function applyTextureParameters(gl, w, h, wrap, filter, vflip) {
|
|
291
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, vflip ? 1 : 0);
|
|
292
|
+
const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
|
|
293
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
|
|
294
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
|
|
295
|
+
if (filter === "mipmap") {
|
|
296
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
297
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
298
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
299
|
+
} else if (filter === "nearest") {
|
|
300
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
301
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
302
|
+
} else {
|
|
303
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
304
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
305
|
+
}
|
|
306
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
|
|
307
|
+
}
|
|
169
308
|
function uploadElement(gl, texture, unit, el) {
|
|
170
309
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
171
310
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
172
311
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
|
|
173
312
|
}
|
|
174
|
-
function createTexture(gl,
|
|
313
|
+
function createTexture(gl, input, unit) {
|
|
314
|
+
const opts = resolveOptions(normalizeTextureInput(input));
|
|
315
|
+
const source = opts.src;
|
|
175
316
|
const texture = initTexture(gl, unit);
|
|
176
317
|
if (typeof source === "string") {
|
|
177
318
|
gl.texImage2D(
|
|
@@ -203,10 +344,7 @@ function createTexture(gl, source, unit) {
|
|
|
203
344
|
return;
|
|
204
345
|
}
|
|
205
346
|
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
|
-
}
|
|
347
|
+
applyTextureParameters(gl, img.width, img.height, opts.wrap, opts.filter, opts.vflip);
|
|
210
348
|
state2.width = img.width;
|
|
211
349
|
state2.height = img.height;
|
|
212
350
|
state2.loaded = true;
|
|
@@ -229,6 +367,7 @@ function createTexture(gl, source, unit) {
|
|
|
229
367
|
};
|
|
230
368
|
if (source.complete && source.naturalWidth > 0) {
|
|
231
369
|
uploadElement(gl, texture, unit, source);
|
|
370
|
+
applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
|
|
232
371
|
state2.width = source.naturalWidth;
|
|
233
372
|
state2.height = source.naturalHeight;
|
|
234
373
|
return { state: state2, promise: null };
|
|
@@ -240,6 +379,7 @@ function createTexture(gl, source, unit) {
|
|
|
240
379
|
return;
|
|
241
380
|
}
|
|
242
381
|
uploadElement(gl, texture, unit, source);
|
|
382
|
+
applyTextureParameters(gl, source.naturalWidth, source.naturalHeight, opts.wrap, opts.filter, opts.vflip);
|
|
243
383
|
state2.width = source.naturalWidth;
|
|
244
384
|
state2.height = source.naturalHeight;
|
|
245
385
|
state2.loaded = true;
|
|
@@ -254,6 +394,7 @@ function createTexture(gl, source, unit) {
|
|
|
254
394
|
const h = source.videoHeight || 1;
|
|
255
395
|
if (source.readyState >= 2) {
|
|
256
396
|
uploadElement(gl, texture, unit, source);
|
|
397
|
+
applyTextureParameters(gl, w, h, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
|
|
257
398
|
} else {
|
|
258
399
|
gl.texImage2D(
|
|
259
400
|
gl.TEXTURE_2D,
|
|
@@ -279,6 +420,7 @@ function createTexture(gl, source, unit) {
|
|
|
279
420
|
return { state: state2, promise: null };
|
|
280
421
|
}
|
|
281
422
|
uploadElement(gl, texture, unit, source);
|
|
423
|
+
applyTextureParameters(gl, source.width, source.height, opts.wrap, opts.filter === "mipmap" ? "linear" : opts.filter, opts.vflip);
|
|
282
424
|
const state = {
|
|
283
425
|
texture,
|
|
284
426
|
width: source.width,
|
|
@@ -325,63 +467,213 @@ function disposeTextures(gl, textures) {
|
|
|
325
467
|
}
|
|
326
468
|
|
|
327
469
|
// src/uniforms.ts
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (locations.
|
|
332
|
-
|
|
470
|
+
function setUniforms(gl, locations, time, delta, frame, width, height, mouse, channelRes) {
|
|
471
|
+
if (locations.iTime) gl.uniform1f(locations.iTime, time);
|
|
472
|
+
if (locations.iTimeDelta) gl.uniform1f(locations.iTimeDelta, delta);
|
|
473
|
+
if (locations.iFrame) gl.uniform1i(locations.iFrame, frame);
|
|
474
|
+
if (locations.iResolution) gl.uniform3f(locations.iResolution, width, height, 1);
|
|
475
|
+
if (locations.iMouse) {
|
|
476
|
+
const mz = mouse.pressed ? mouse.clickX : -Math.abs(mouse.clickX);
|
|
477
|
+
const mw = mouse.pressed ? mouse.clickY : -Math.abs(mouse.clickY);
|
|
478
|
+
gl.uniform4f(locations.iMouse, mouse.x, mouse.y, mz, mw);
|
|
333
479
|
}
|
|
334
|
-
if (locations.
|
|
335
|
-
gl.
|
|
480
|
+
if (locations.iChannelResolution && channelRes) {
|
|
481
|
+
gl.uniform3fv(locations.iChannelResolution, channelRes);
|
|
336
482
|
}
|
|
483
|
+
if (locations.iDate) {
|
|
484
|
+
const now = /* @__PURE__ */ new Date();
|
|
485
|
+
const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1e3;
|
|
486
|
+
gl.uniform4f(locations.iDate, now.getFullYear(), now.getMonth(), now.getDate(), seconds);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function updateUniforms(state, delta, speed, mouse) {
|
|
490
|
+
state.time += delta * speed;
|
|
337
491
|
state.frame++;
|
|
338
|
-
|
|
339
|
-
|
|
492
|
+
const res = new Float32Array(12);
|
|
493
|
+
for (let i = 0; i < 4; i++) {
|
|
494
|
+
const tex = state.textures[i];
|
|
495
|
+
if (tex) {
|
|
496
|
+
res[i * 3] = tex.width;
|
|
497
|
+
res[i * 3 + 1] = tex.height;
|
|
498
|
+
res[i * 3 + 2] = 1;
|
|
499
|
+
}
|
|
340
500
|
}
|
|
341
|
-
|
|
342
|
-
gl
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
501
|
+
setUniforms(
|
|
502
|
+
state.gl,
|
|
503
|
+
state.locations,
|
|
504
|
+
state.time,
|
|
505
|
+
delta,
|
|
506
|
+
state.frame,
|
|
507
|
+
state.gl.drawingBufferWidth,
|
|
508
|
+
state.gl.drawingBufferHeight,
|
|
509
|
+
mouse,
|
|
510
|
+
res
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/multipass.ts
|
|
515
|
+
var PASS_ORDER = ["BufferA", "BufferB", "BufferC", "BufferD", "Image"];
|
|
516
|
+
var CHANNEL_KEYS = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
|
|
517
|
+
function isPassName(v) {
|
|
518
|
+
return typeof v === "string" && PASS_ORDER.includes(v);
|
|
519
|
+
}
|
|
520
|
+
function createPingPongTextures(gl, w, h) {
|
|
521
|
+
const textures = [];
|
|
522
|
+
for (let i = 0; i < 2; i++) {
|
|
523
|
+
const tex = gl.createTexture();
|
|
524
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
525
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
526
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
527
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
528
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
529
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
530
|
+
textures.push(tex);
|
|
348
531
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
532
|
+
return textures;
|
|
533
|
+
}
|
|
534
|
+
function createFBO(gl, texture) {
|
|
535
|
+
const fbo = gl.createFramebuffer();
|
|
536
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
537
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
538
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
539
|
+
return fbo;
|
|
540
|
+
}
|
|
541
|
+
function createMultipassRenderer(gl, config, externalTextures) {
|
|
542
|
+
const w = gl.drawingBufferWidth || 1;
|
|
543
|
+
const h = gl.drawingBufferHeight || 1;
|
|
544
|
+
const passes = [];
|
|
545
|
+
for (const name of PASS_ORDER) {
|
|
546
|
+
const passConfig = config[name];
|
|
547
|
+
if (!passConfig) continue;
|
|
548
|
+
const program = createProgram(gl, passConfig.code);
|
|
549
|
+
if (typeof program === "string") return `${name}: ${program}`;
|
|
550
|
+
setupQuad(gl, program);
|
|
551
|
+
const locations = getUniformLocations(gl, program);
|
|
552
|
+
const isImage = name === "Image";
|
|
553
|
+
const pingPong = isImage ? null : createPingPongTextures(gl, w, h);
|
|
554
|
+
const fbo = isImage ? null : createFBO(gl, pingPong[0]);
|
|
555
|
+
const channelBindings = [null, null, null, null];
|
|
556
|
+
for (let i = 0; i < 4; i++) {
|
|
557
|
+
const input = passConfig[CHANNEL_KEYS[i]];
|
|
558
|
+
if (input == null) continue;
|
|
559
|
+
if (isPassName(input)) {
|
|
560
|
+
channelBindings[i] = { passRef: input };
|
|
561
|
+
} else {
|
|
562
|
+
channelBindings[i] = externalTextures[i];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
passes.push({
|
|
566
|
+
name,
|
|
567
|
+
program,
|
|
568
|
+
locations,
|
|
569
|
+
fbo,
|
|
570
|
+
pingPong,
|
|
571
|
+
currentIdx: 0,
|
|
572
|
+
width: w,
|
|
573
|
+
height: h,
|
|
574
|
+
channelBindings
|
|
575
|
+
});
|
|
353
576
|
}
|
|
354
|
-
if (
|
|
355
|
-
|
|
577
|
+
if (passes.length === 0) return "No passes defined";
|
|
578
|
+
return passes;
|
|
579
|
+
}
|
|
580
|
+
function renderMultipass(gl, passes, delta, speed, mouse, sharedState) {
|
|
581
|
+
sharedState.time += delta * speed;
|
|
582
|
+
sharedState.frame++;
|
|
583
|
+
const passTextures = {};
|
|
584
|
+
for (const pass of passes) {
|
|
585
|
+
const isImage = pass.name === "Image";
|
|
586
|
+
if (pass.pingPong) {
|
|
587
|
+
const writeIdx = pass.currentIdx;
|
|
588
|
+
const readIdx = 1 - writeIdx;
|
|
589
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, pass.fbo);
|
|
590
|
+
gl.framebufferTexture2D(
|
|
591
|
+
gl.FRAMEBUFFER,
|
|
592
|
+
gl.COLOR_ATTACHMENT0,
|
|
593
|
+
gl.TEXTURE_2D,
|
|
594
|
+
pass.pingPong[writeIdx],
|
|
595
|
+
0
|
|
596
|
+
);
|
|
597
|
+
passTextures[pass.name] = pass.pingPong[readIdx];
|
|
598
|
+
}
|
|
599
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, isImage ? null : pass.fbo);
|
|
600
|
+
gl.viewport(0, 0, isImage ? gl.drawingBufferWidth : pass.width, isImage ? gl.drawingBufferHeight : pass.height);
|
|
601
|
+
gl.useProgram(pass.program);
|
|
602
|
+
const tempTextures = [null, null, null, null];
|
|
356
603
|
for (let i = 0; i < 4; i++) {
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
604
|
+
const binding = pass.channelBindings[i];
|
|
605
|
+
if (!binding) continue;
|
|
606
|
+
if ("passRef" in binding) {
|
|
607
|
+
const refTex = passTextures[binding.passRef];
|
|
608
|
+
if (refTex) {
|
|
609
|
+
gl.activeTexture(gl.TEXTURE0 + i);
|
|
610
|
+
gl.bindTexture(gl.TEXTURE_2D, refTex);
|
|
611
|
+
if (pass.locations.iChannel[i]) {
|
|
612
|
+
gl.uniform1i(pass.locations.iChannel[i], i);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
tempTextures[i] = binding;
|
|
362
617
|
}
|
|
363
618
|
}
|
|
364
|
-
gl.
|
|
619
|
+
bindTextures(gl, pass.locations.iChannel, tempTextures);
|
|
620
|
+
const channelRes = new Float32Array(12);
|
|
621
|
+
for (let i = 0; i < 4; i++) {
|
|
622
|
+
const binding = pass.channelBindings[i];
|
|
623
|
+
if (!binding) continue;
|
|
624
|
+
if ("passRef" in binding) {
|
|
625
|
+
const refPass = passes.find((p) => p.name === binding.passRef);
|
|
626
|
+
if (refPass) {
|
|
627
|
+
channelRes[i * 3] = refPass.width;
|
|
628
|
+
channelRes[i * 3 + 1] = refPass.height;
|
|
629
|
+
channelRes[i * 3 + 2] = 1;
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
channelRes[i * 3] = binding.width;
|
|
633
|
+
channelRes[i * 3 + 1] = binding.height;
|
|
634
|
+
channelRes[i * 3 + 2] = 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const vw = isImage ? gl.drawingBufferWidth : pass.width;
|
|
638
|
+
const vh = isImage ? gl.drawingBufferHeight : pass.height;
|
|
639
|
+
setUniforms(gl, pass.locations, sharedState.time, delta, sharedState.frame, vw, vh, mouse, channelRes);
|
|
640
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
641
|
+
if (pass.pingPong) {
|
|
642
|
+
pass.currentIdx = 1 - pass.currentIdx;
|
|
643
|
+
}
|
|
365
644
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
645
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
646
|
+
}
|
|
647
|
+
function resizeFBOs(gl, passes, w, h) {
|
|
648
|
+
for (const pass of passes) {
|
|
649
|
+
if (!pass.pingPong) continue;
|
|
650
|
+
pass.width = w;
|
|
651
|
+
pass.height = h;
|
|
652
|
+
for (let i = 0; i < 2; i++) {
|
|
653
|
+
gl.bindTexture(gl.TEXTURE_2D, pass.pingPong[i]);
|
|
654
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function disposeMultipass(gl, passes) {
|
|
659
|
+
for (const pass of passes) {
|
|
660
|
+
gl.deleteProgram(pass.program);
|
|
661
|
+
if (pass.fbo) gl.deleteFramebuffer(pass.fbo);
|
|
662
|
+
if (pass.pingPong) {
|
|
663
|
+
gl.deleteTexture(pass.pingPong[0]);
|
|
664
|
+
gl.deleteTexture(pass.pingPong[1]);
|
|
665
|
+
}
|
|
377
666
|
}
|
|
378
667
|
}
|
|
379
668
|
|
|
380
669
|
// src/useShadertoy.ts
|
|
381
|
-
var
|
|
670
|
+
var CHANNEL_KEYS2 = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
|
|
382
671
|
function useShadertoy({
|
|
383
672
|
fragmentShader,
|
|
673
|
+
passes: passesProp,
|
|
384
674
|
textures: texturesProp,
|
|
675
|
+
id,
|
|
676
|
+
apiKey,
|
|
385
677
|
paused = false,
|
|
386
678
|
speed = 1,
|
|
387
679
|
pixelRatio,
|
|
@@ -391,11 +683,14 @@ function useShadertoy({
|
|
|
391
683
|
}) {
|
|
392
684
|
const canvasRef = (0, import_react.useRef)(null);
|
|
393
685
|
const rendererRef = (0, import_react.useRef)(null);
|
|
686
|
+
const multipassRef = (0, import_react.useRef)(null);
|
|
394
687
|
const rafRef = (0, import_react.useRef)(0);
|
|
395
688
|
const pausedRef = (0, import_react.useRef)(paused);
|
|
396
689
|
const speedRef = (0, import_react.useRef)(speed);
|
|
397
690
|
const [isReady, setIsReady] = (0, import_react.useState)(false);
|
|
398
691
|
const [error, setError] = (0, import_react.useState)(null);
|
|
692
|
+
const [meta, setMeta] = (0, import_react.useState)(null);
|
|
693
|
+
const [resolved, setResolved] = (0, import_react.useState)(id ? null : { passes: passesProp, textures: texturesProp, fragmentShader });
|
|
399
694
|
const mouseState = (0, import_react.useRef)({
|
|
400
695
|
x: 0,
|
|
401
696
|
y: 0,
|
|
@@ -403,25 +698,68 @@ function useShadertoy({
|
|
|
403
698
|
clickY: 0,
|
|
404
699
|
pressed: false
|
|
405
700
|
});
|
|
701
|
+
const sharedState = (0, import_react.useRef)({ time: 0, frame: 0 });
|
|
406
702
|
pausedRef.current = paused;
|
|
407
703
|
speedRef.current = speed;
|
|
408
704
|
(0, import_react.useEffect)(() => {
|
|
705
|
+
if (!id) return;
|
|
706
|
+
if (!apiKey) {
|
|
707
|
+
setError("apiKey is required when using id");
|
|
708
|
+
onError?.("apiKey is required when using id");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
let cancelled = false;
|
|
712
|
+
fetchShader(id, apiKey).then((shader) => {
|
|
713
|
+
if (cancelled) return;
|
|
714
|
+
const config = apiToConfig(shader);
|
|
715
|
+
setMeta(config.meta);
|
|
716
|
+
if (isSinglePass(config.passes)) {
|
|
717
|
+
const imagePass = config.passes.Image;
|
|
718
|
+
setResolved({
|
|
719
|
+
fragmentShader: imagePass.code,
|
|
720
|
+
textures: config.textures
|
|
721
|
+
});
|
|
722
|
+
} else {
|
|
723
|
+
setResolved({ passes: config.passes });
|
|
724
|
+
}
|
|
725
|
+
}).catch((err) => {
|
|
726
|
+
if (cancelled) return;
|
|
727
|
+
const msg = err instanceof Error ? err.message : "Failed to fetch shader";
|
|
728
|
+
setError(msg);
|
|
729
|
+
onError?.(msg);
|
|
730
|
+
});
|
|
731
|
+
return () => {
|
|
732
|
+
cancelled = true;
|
|
733
|
+
};
|
|
734
|
+
}, [id, apiKey]);
|
|
735
|
+
const effectivePasses = resolved?.passes;
|
|
736
|
+
const effectiveTextures = resolved?.textures ?? texturesProp;
|
|
737
|
+
const effectiveShader = resolved?.fragmentShader ?? fragmentShader;
|
|
738
|
+
const isMultipass = !!effectivePasses;
|
|
739
|
+
(0, import_react.useEffect)(() => {
|
|
740
|
+
if (id && !resolved) return;
|
|
409
741
|
const canvas = canvasRef.current;
|
|
410
742
|
if (!canvas) return;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
743
|
+
sharedState.current = { time: 0, frame: 0 };
|
|
744
|
+
const gl = canvas.getContext("webgl2", {
|
|
745
|
+
antialias: false,
|
|
746
|
+
alpha: true,
|
|
747
|
+
premultipliedAlpha: false
|
|
748
|
+
});
|
|
749
|
+
if (!gl) {
|
|
750
|
+
const msg = "WebGL2 not supported";
|
|
751
|
+
setError(msg);
|
|
752
|
+
onError?.(msg);
|
|
415
753
|
return;
|
|
416
754
|
}
|
|
417
|
-
|
|
755
|
+
const externalTextures = [null, null, null, null];
|
|
418
756
|
const texturePromises = [];
|
|
419
|
-
if (
|
|
757
|
+
if (effectiveTextures) {
|
|
420
758
|
for (let i = 0; i < 4; i++) {
|
|
421
|
-
const src =
|
|
759
|
+
const src = effectiveTextures[CHANNEL_KEYS2[i]];
|
|
422
760
|
if (src != null) {
|
|
423
|
-
const { state, promise } = createTexture(
|
|
424
|
-
|
|
761
|
+
const { state, promise } = createTexture(gl, src, i);
|
|
762
|
+
externalTextures[i] = state;
|
|
425
763
|
if (promise) texturePromises.push(promise);
|
|
426
764
|
}
|
|
427
765
|
}
|
|
@@ -431,41 +769,88 @@ function useShadertoy({
|
|
|
431
769
|
setError(null);
|
|
432
770
|
onLoad?.();
|
|
433
771
|
};
|
|
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);
|
|
772
|
+
const handleError = (msg) => {
|
|
773
|
+
setError(msg);
|
|
774
|
+
onError?.(msg);
|
|
775
|
+
};
|
|
776
|
+
if (isMultipass) {
|
|
777
|
+
const passResult = createMultipassRenderer(gl, effectivePasses, externalTextures);
|
|
778
|
+
if (typeof passResult === "string") {
|
|
779
|
+
handleError(passResult);
|
|
780
|
+
return;
|
|
455
781
|
}
|
|
782
|
+
multipassRef.current = passResult;
|
|
783
|
+
rendererRef.current = null;
|
|
784
|
+
if (texturePromises.length > 0) {
|
|
785
|
+
Promise.all(texturePromises).then(() => {
|
|
786
|
+
if (multipassRef.current) markReady();
|
|
787
|
+
}).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
|
|
788
|
+
} else {
|
|
789
|
+
markReady();
|
|
790
|
+
}
|
|
791
|
+
let lastTimestamp = 0;
|
|
792
|
+
const loop = (timestamp) => {
|
|
793
|
+
const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
|
|
794
|
+
lastTimestamp = timestamp;
|
|
795
|
+
if (!pausedRef.current && multipassRef.current) {
|
|
796
|
+
updateDynamicTextures(gl, externalTextures);
|
|
797
|
+
renderMultipass(gl, multipassRef.current, delta, speedRef.current, mouseState.current, sharedState.current);
|
|
798
|
+
}
|
|
799
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
800
|
+
};
|
|
456
801
|
rafRef.current = requestAnimationFrame(loop);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
802
|
+
return () => {
|
|
803
|
+
cancelAnimationFrame(rafRef.current);
|
|
804
|
+
if (multipassRef.current) {
|
|
805
|
+
disposeMultipass(gl, multipassRef.current);
|
|
806
|
+
multipassRef.current = null;
|
|
807
|
+
}
|
|
808
|
+
disposeTextures(gl, externalTextures);
|
|
809
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
810
|
+
setIsReady(false);
|
|
811
|
+
};
|
|
812
|
+
} else {
|
|
813
|
+
const shaderCode = effectiveShader || "void mainImage(out vec4 c, in vec2 f){ c = vec4(0); }";
|
|
814
|
+
const result = createRenderer(canvas, shaderCode);
|
|
815
|
+
if (typeof result === "string") {
|
|
816
|
+
handleError(result);
|
|
817
|
+
return;
|
|
465
818
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
819
|
+
rendererRef.current = result;
|
|
820
|
+
multipassRef.current = null;
|
|
821
|
+
result.textures = externalTextures;
|
|
822
|
+
if (texturePromises.length > 0) {
|
|
823
|
+
Promise.all(texturePromises).then(() => {
|
|
824
|
+
if (rendererRef.current) markReady();
|
|
825
|
+
}).catch((err) => handleError(err instanceof Error ? err.message : "Texture load failed"));
|
|
826
|
+
} else {
|
|
827
|
+
markReady();
|
|
828
|
+
}
|
|
829
|
+
let lastTimestamp = 0;
|
|
830
|
+
const loop = (timestamp) => {
|
|
831
|
+
const delta = lastTimestamp ? (timestamp - lastTimestamp) / 1e3 : 0;
|
|
832
|
+
lastTimestamp = timestamp;
|
|
833
|
+
if (!pausedRef.current && rendererRef.current) {
|
|
834
|
+
const r = rendererRef.current;
|
|
835
|
+
updateDynamicTextures(r.gl, r.textures);
|
|
836
|
+
bindTextures(r.gl, r.locations.iChannel, r.textures);
|
|
837
|
+
updateUniforms(r, delta, speedRef.current, mouseState.current);
|
|
838
|
+
render(r);
|
|
839
|
+
}
|
|
840
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
841
|
+
};
|
|
842
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
843
|
+
return () => {
|
|
844
|
+
cancelAnimationFrame(rafRef.current);
|
|
845
|
+
if (rendererRef.current) {
|
|
846
|
+
disposeTextures(rendererRef.current.gl, rendererRef.current.textures);
|
|
847
|
+
dispose(rendererRef.current);
|
|
848
|
+
rendererRef.current = null;
|
|
849
|
+
}
|
|
850
|
+
setIsReady(false);
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}, [effectiveShader, effectivePasses, effectiveTextures, resolved, onError, onLoad]);
|
|
469
854
|
(0, import_react.useEffect)(() => {
|
|
470
855
|
const canvas = canvasRef.current;
|
|
471
856
|
if (!canvas) return;
|
|
@@ -473,8 +858,14 @@ function useShadertoy({
|
|
|
473
858
|
const observer = new ResizeObserver((entries) => {
|
|
474
859
|
for (const entry of entries) {
|
|
475
860
|
const { width, height } = entry.contentRect;
|
|
476
|
-
|
|
477
|
-
|
|
861
|
+
const w = Math.round(width * dpr);
|
|
862
|
+
const h = Math.round(height * dpr);
|
|
863
|
+
canvas.width = w;
|
|
864
|
+
canvas.height = h;
|
|
865
|
+
if (multipassRef.current) {
|
|
866
|
+
const gl = canvas.getContext("webgl2");
|
|
867
|
+
if (gl) resizeFBOs(gl, multipassRef.current, w, h);
|
|
868
|
+
}
|
|
478
869
|
}
|
|
479
870
|
});
|
|
480
871
|
observer.observe(canvas);
|
|
@@ -541,14 +932,18 @@ function useShadertoy({
|
|
|
541
932
|
const resume = (0, import_react.useCallback)(() => {
|
|
542
933
|
pausedRef.current = false;
|
|
543
934
|
}, []);
|
|
544
|
-
return { canvasRef, isReady, error, pause, resume };
|
|
935
|
+
return { canvasRef, isReady, error, pause, resume, meta };
|
|
545
936
|
}
|
|
546
937
|
|
|
547
938
|
// src/Shadertoy.tsx
|
|
548
939
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
549
940
|
function Shadertoy({
|
|
550
941
|
fragmentShader,
|
|
942
|
+
passes,
|
|
551
943
|
textures,
|
|
944
|
+
id,
|
|
945
|
+
apiKey,
|
|
946
|
+
showLicense,
|
|
552
947
|
style,
|
|
553
948
|
className,
|
|
554
949
|
paused,
|
|
@@ -558,9 +953,12 @@ function Shadertoy({
|
|
|
558
953
|
onError,
|
|
559
954
|
onLoad
|
|
560
955
|
}) {
|
|
561
|
-
const { canvasRef } = useShadertoy({
|
|
956
|
+
const { canvasRef, meta } = useShadertoy({
|
|
562
957
|
fragmentShader,
|
|
958
|
+
passes,
|
|
563
959
|
textures,
|
|
960
|
+
id,
|
|
961
|
+
apiKey,
|
|
564
962
|
paused,
|
|
565
963
|
speed,
|
|
566
964
|
pixelRatio,
|
|
@@ -568,6 +966,35 @@ function Shadertoy({
|
|
|
568
966
|
onError,
|
|
569
967
|
onLoad
|
|
570
968
|
});
|
|
969
|
+
const shouldShowLicense = showLicense ?? !!id;
|
|
970
|
+
const hasMeta = shouldShowLicense && meta;
|
|
971
|
+
if (hasMeta) {
|
|
972
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative", ...style }, className, children: [
|
|
973
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
974
|
+
"canvas",
|
|
975
|
+
{
|
|
976
|
+
ref: canvasRef,
|
|
977
|
+
style: { width: "100%", height: "100%", display: "block" }
|
|
978
|
+
}
|
|
979
|
+
),
|
|
980
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
|
|
981
|
+
position: "absolute",
|
|
982
|
+
bottom: 8,
|
|
983
|
+
right: 8,
|
|
984
|
+
background: "rgba(0,0,0,0.6)",
|
|
985
|
+
color: "#fff",
|
|
986
|
+
padding: "4px 10px",
|
|
987
|
+
borderRadius: 4,
|
|
988
|
+
fontSize: 12,
|
|
989
|
+
fontFamily: "system-ui, sans-serif",
|
|
990
|
+
pointerEvents: "none"
|
|
991
|
+
}, children: [
|
|
992
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: meta.name }),
|
|
993
|
+
" by ",
|
|
994
|
+
meta.author
|
|
995
|
+
] })
|
|
996
|
+
] });
|
|
997
|
+
}
|
|
571
998
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
572
999
|
"canvas",
|
|
573
1000
|
{
|
|
@@ -580,5 +1007,7 @@ function Shadertoy({
|
|
|
580
1007
|
// Annotate the CommonJS export names for ESM import in node:
|
|
581
1008
|
0 && (module.exports = {
|
|
582
1009
|
Shadertoy,
|
|
1010
|
+
apiToConfig,
|
|
1011
|
+
fetchShader,
|
|
583
1012
|
useShadertoy
|
|
584
1013
|
});
|