react-shadertoy 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { CSSProperties, RefObject } from 'react';
3
3
 
4
- /** Texture source: URL string for image loading */
5
- type TextureSource = string;
4
+ /** Texture source: URL string, or an HTML element for dynamic textures */
5
+ type TextureSource = string | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement;
6
6
  /** Texture inputs mapped to Shadertoy channels */
7
7
  type TextureInputs = {
8
8
  iChannel0?: TextureSource;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { CSSProperties, RefObject } from 'react';
3
3
 
4
- /** Texture source: URL string for image loading */
5
- type TextureSource = string;
4
+ /** Texture source: URL string, or an HTML element for dynamic textures */
5
+ type TextureSource = string | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement;
6
6
  /** Texture inputs mapped to Shadertoy channels */
7
7
  type TextureInputs = {
8
8
  iChannel0?: TextureSource;
package/dist/index.js CHANGED
@@ -153,50 +153,159 @@ function dispose(state) {
153
153
  }
154
154
 
155
155
  // src/textures.ts
156
- function loadImageTexture(gl, url, unit) {
156
+ function isPOT(v) {
157
+ return (v & v - 1) === 0 && v > 0;
158
+ }
159
+ function initTexture(gl, unit) {
157
160
  const texture = gl.createTexture();
158
161
  gl.activeTexture(gl.TEXTURE0 + unit);
159
162
  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
163
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
172
164
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
173
165
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
174
166
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
175
- const state = { texture, width: 1, height: 1, unit, loaded: false };
176
- const promise = new Promise((resolve, reject) => {
177
- const img = new Image();
178
- img.crossOrigin = "anonymous";
179
- img.onload = () => {
180
- if (gl.isContextLost()) {
167
+ return texture;
168
+ }
169
+ function uploadElement(gl, texture, unit, el) {
170
+ gl.activeTexture(gl.TEXTURE0 + unit);
171
+ gl.bindTexture(gl.TEXTURE_2D, texture);
172
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
173
+ }
174
+ function createTexture(gl, source, unit) {
175
+ const texture = initTexture(gl, unit);
176
+ if (typeof source === "string") {
177
+ gl.texImage2D(
178
+ gl.TEXTURE_2D,
179
+ 0,
180
+ gl.RGBA,
181
+ 1,
182
+ 1,
183
+ 0,
184
+ gl.RGBA,
185
+ gl.UNSIGNED_BYTE,
186
+ new Uint8Array([255, 0, 255, 255])
187
+ );
188
+ const state2 = {
189
+ texture,
190
+ width: 1,
191
+ height: 1,
192
+ unit,
193
+ loaded: false,
194
+ needsUpdate: false,
195
+ source
196
+ };
197
+ const promise = new Promise((resolve, reject) => {
198
+ const img = new Image();
199
+ img.crossOrigin = "anonymous";
200
+ img.onload = () => {
201
+ if (gl.isContextLost()) {
202
+ resolve();
203
+ return;
204
+ }
205
+ uploadElement(gl, texture, unit, img);
206
+ if (isPOT(img.width) && isPOT(img.height)) {
207
+ gl.generateMipmap(gl.TEXTURE_2D);
208
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
209
+ }
210
+ state2.width = img.width;
211
+ state2.height = img.height;
212
+ state2.loaded = true;
181
213
  resolve();
182
- return;
183
- }
184
- gl.activeTexture(gl.TEXTURE0 + unit);
185
- gl.bindTexture(gl.TEXTURE_2D, texture);
186
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
187
- if (isPOT(img.width) && isPOT(img.height)) {
188
- gl.generateMipmap(gl.TEXTURE_2D);
189
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
190
- }
191
- state.width = img.width;
192
- state.height = img.height;
193
- state.loaded = true;
194
- resolve();
214
+ };
215
+ img.onerror = () => reject(new Error(`Failed to load texture: ${source}`));
216
+ img.src = source;
217
+ });
218
+ return { state: state2, promise };
219
+ }
220
+ if (source instanceof HTMLImageElement) {
221
+ const state2 = {
222
+ texture,
223
+ width: source.naturalWidth || 1,
224
+ height: source.naturalHeight || 1,
225
+ unit,
226
+ loaded: source.complete,
227
+ needsUpdate: false,
228
+ source
195
229
  };
196
- img.onerror = () => reject(new Error(`Failed to load texture: ${url}`));
197
- img.src = url;
198
- });
199
- return { state, promise };
230
+ if (source.complete && source.naturalWidth > 0) {
231
+ uploadElement(gl, texture, unit, source);
232
+ state2.width = source.naturalWidth;
233
+ state2.height = source.naturalHeight;
234
+ return { state: state2, promise: null };
235
+ }
236
+ const promise = new Promise((resolve, reject) => {
237
+ source.onload = () => {
238
+ if (gl.isContextLost()) {
239
+ resolve();
240
+ return;
241
+ }
242
+ uploadElement(gl, texture, unit, source);
243
+ state2.width = source.naturalWidth;
244
+ state2.height = source.naturalHeight;
245
+ state2.loaded = true;
246
+ resolve();
247
+ };
248
+ source.onerror = () => reject(new Error("Failed to load image element"));
249
+ });
250
+ return { state: state2, promise };
251
+ }
252
+ if (source instanceof HTMLVideoElement) {
253
+ const w = source.videoWidth || 1;
254
+ const h = source.videoHeight || 1;
255
+ if (source.readyState >= 2) {
256
+ uploadElement(gl, texture, unit, source);
257
+ } else {
258
+ gl.texImage2D(
259
+ gl.TEXTURE_2D,
260
+ 0,
261
+ gl.RGBA,
262
+ 1,
263
+ 1,
264
+ 0,
265
+ gl.RGBA,
266
+ gl.UNSIGNED_BYTE,
267
+ new Uint8Array([0, 0, 0, 255])
268
+ );
269
+ }
270
+ const state2 = {
271
+ texture,
272
+ width: w,
273
+ height: h,
274
+ unit,
275
+ loaded: source.readyState >= 2,
276
+ needsUpdate: true,
277
+ source
278
+ };
279
+ return { state: state2, promise: null };
280
+ }
281
+ uploadElement(gl, texture, unit, source);
282
+ const state = {
283
+ texture,
284
+ width: source.width,
285
+ height: source.height,
286
+ unit,
287
+ loaded: true,
288
+ needsUpdate: true,
289
+ source
290
+ };
291
+ return { state, promise: null };
292
+ }
293
+ function updateDynamicTextures(gl, textures) {
294
+ for (const tex of textures) {
295
+ if (!tex || !tex.needsUpdate || !tex.source) continue;
296
+ if (tex.source instanceof HTMLVideoElement) {
297
+ const v = tex.source;
298
+ if (v.readyState < 2) continue;
299
+ uploadElement(gl, tex.texture, tex.unit, v);
300
+ tex.width = v.videoWidth;
301
+ tex.height = v.videoHeight;
302
+ tex.loaded = true;
303
+ } else if (tex.source instanceof HTMLCanvasElement) {
304
+ uploadElement(gl, tex.texture, tex.unit, tex.source);
305
+ tex.width = tex.source.width;
306
+ tex.height = tex.source.height;
307
+ }
308
+ }
200
309
  }
201
310
  function bindTextures(gl, locations, textures) {
202
311
  for (let i = 0; i < 4; i++) {
@@ -214,9 +323,6 @@ function disposeTextures(gl, textures) {
214
323
  if (tex) gl.deleteTexture(tex.texture);
215
324
  }
216
325
  }
217
- function isPOT(v) {
218
- return (v & v - 1) === 0 && v > 0;
219
- }
220
326
 
221
327
  // src/uniforms.ts
222
328
  function updateUniforms(state, delta, speed, mouse) {
@@ -313,10 +419,10 @@ function useShadertoy({
313
419
  if (texturesProp) {
314
420
  for (let i = 0; i < 4; i++) {
315
421
  const src = texturesProp[CHANNEL_KEYS[i]];
316
- if (typeof src === "string") {
317
- const { state, promise } = loadImageTexture(result.gl, src, i);
422
+ if (src != null) {
423
+ const { state, promise } = createTexture(result.gl, src, i);
318
424
  result.textures[i] = state;
319
- texturePromises.push(promise);
425
+ if (promise) texturePromises.push(promise);
320
426
  }
321
427
  }
322
428
  }
@@ -342,6 +448,7 @@ function useShadertoy({
342
448
  lastTimestamp = timestamp;
343
449
  if (!pausedRef.current && rendererRef.current) {
344
450
  const r = rendererRef.current;
451
+ updateDynamicTextures(r.gl, r.textures);
345
452
  bindTextures(r.gl, r.locations.iChannel, r.textures);
346
453
  updateUniforms(r, delta, speedRef.current, mouseState.current);
347
454
  render(r);
package/dist/index.mjs CHANGED
@@ -126,50 +126,159 @@ function dispose(state) {
126
126
  }
127
127
 
128
128
  // src/textures.ts
129
- function loadImageTexture(gl, url, unit) {
129
+ function isPOT(v) {
130
+ return (v & v - 1) === 0 && v > 0;
131
+ }
132
+ function initTexture(gl, unit) {
130
133
  const texture = gl.createTexture();
131
134
  gl.activeTexture(gl.TEXTURE0 + unit);
132
135
  gl.bindTexture(gl.TEXTURE_2D, texture);
133
- gl.texImage2D(
134
- gl.TEXTURE_2D,
135
- 0,
136
- gl.RGBA,
137
- 1,
138
- 1,
139
- 0,
140
- gl.RGBA,
141
- gl.UNSIGNED_BYTE,
142
- new Uint8Array([255, 0, 255, 255])
143
- );
144
136
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
145
137
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
146
138
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
147
139
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
148
- const state = { texture, width: 1, height: 1, unit, loaded: false };
149
- const promise = new Promise((resolve, reject) => {
150
- const img = new Image();
151
- img.crossOrigin = "anonymous";
152
- img.onload = () => {
153
- if (gl.isContextLost()) {
140
+ return texture;
141
+ }
142
+ function uploadElement(gl, texture, unit, el) {
143
+ gl.activeTexture(gl.TEXTURE0 + unit);
144
+ gl.bindTexture(gl.TEXTURE_2D, texture);
145
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
146
+ }
147
+ function createTexture(gl, source, unit) {
148
+ const texture = initTexture(gl, unit);
149
+ if (typeof source === "string") {
150
+ gl.texImage2D(
151
+ gl.TEXTURE_2D,
152
+ 0,
153
+ gl.RGBA,
154
+ 1,
155
+ 1,
156
+ 0,
157
+ gl.RGBA,
158
+ gl.UNSIGNED_BYTE,
159
+ new Uint8Array([255, 0, 255, 255])
160
+ );
161
+ const state2 = {
162
+ texture,
163
+ width: 1,
164
+ height: 1,
165
+ unit,
166
+ loaded: false,
167
+ needsUpdate: false,
168
+ source
169
+ };
170
+ const promise = new Promise((resolve, reject) => {
171
+ const img = new Image();
172
+ img.crossOrigin = "anonymous";
173
+ img.onload = () => {
174
+ if (gl.isContextLost()) {
175
+ resolve();
176
+ return;
177
+ }
178
+ uploadElement(gl, texture, unit, img);
179
+ if (isPOT(img.width) && isPOT(img.height)) {
180
+ gl.generateMipmap(gl.TEXTURE_2D);
181
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
182
+ }
183
+ state2.width = img.width;
184
+ state2.height = img.height;
185
+ state2.loaded = true;
154
186
  resolve();
155
- return;
156
- }
157
- gl.activeTexture(gl.TEXTURE0 + unit);
158
- gl.bindTexture(gl.TEXTURE_2D, texture);
159
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
160
- if (isPOT(img.width) && isPOT(img.height)) {
161
- gl.generateMipmap(gl.TEXTURE_2D);
162
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
163
- }
164
- state.width = img.width;
165
- state.height = img.height;
166
- state.loaded = true;
167
- resolve();
187
+ };
188
+ img.onerror = () => reject(new Error(`Failed to load texture: ${source}`));
189
+ img.src = source;
190
+ });
191
+ return { state: state2, promise };
192
+ }
193
+ if (source instanceof HTMLImageElement) {
194
+ const state2 = {
195
+ texture,
196
+ width: source.naturalWidth || 1,
197
+ height: source.naturalHeight || 1,
198
+ unit,
199
+ loaded: source.complete,
200
+ needsUpdate: false,
201
+ source
168
202
  };
169
- img.onerror = () => reject(new Error(`Failed to load texture: ${url}`));
170
- img.src = url;
171
- });
172
- return { state, promise };
203
+ if (source.complete && source.naturalWidth > 0) {
204
+ uploadElement(gl, texture, unit, source);
205
+ state2.width = source.naturalWidth;
206
+ state2.height = source.naturalHeight;
207
+ return { state: state2, promise: null };
208
+ }
209
+ const promise = new Promise((resolve, reject) => {
210
+ source.onload = () => {
211
+ if (gl.isContextLost()) {
212
+ resolve();
213
+ return;
214
+ }
215
+ uploadElement(gl, texture, unit, source);
216
+ state2.width = source.naturalWidth;
217
+ state2.height = source.naturalHeight;
218
+ state2.loaded = true;
219
+ resolve();
220
+ };
221
+ source.onerror = () => reject(new Error("Failed to load image element"));
222
+ });
223
+ return { state: state2, promise };
224
+ }
225
+ if (source instanceof HTMLVideoElement) {
226
+ const w = source.videoWidth || 1;
227
+ const h = source.videoHeight || 1;
228
+ if (source.readyState >= 2) {
229
+ uploadElement(gl, texture, unit, source);
230
+ } else {
231
+ gl.texImage2D(
232
+ gl.TEXTURE_2D,
233
+ 0,
234
+ gl.RGBA,
235
+ 1,
236
+ 1,
237
+ 0,
238
+ gl.RGBA,
239
+ gl.UNSIGNED_BYTE,
240
+ new Uint8Array([0, 0, 0, 255])
241
+ );
242
+ }
243
+ const state2 = {
244
+ texture,
245
+ width: w,
246
+ height: h,
247
+ unit,
248
+ loaded: source.readyState >= 2,
249
+ needsUpdate: true,
250
+ source
251
+ };
252
+ return { state: state2, promise: null };
253
+ }
254
+ uploadElement(gl, texture, unit, source);
255
+ const state = {
256
+ texture,
257
+ width: source.width,
258
+ height: source.height,
259
+ unit,
260
+ loaded: true,
261
+ needsUpdate: true,
262
+ source
263
+ };
264
+ return { state, promise: null };
265
+ }
266
+ function updateDynamicTextures(gl, textures) {
267
+ for (const tex of textures) {
268
+ if (!tex || !tex.needsUpdate || !tex.source) continue;
269
+ if (tex.source instanceof HTMLVideoElement) {
270
+ const v = tex.source;
271
+ if (v.readyState < 2) continue;
272
+ uploadElement(gl, tex.texture, tex.unit, v);
273
+ tex.width = v.videoWidth;
274
+ tex.height = v.videoHeight;
275
+ tex.loaded = true;
276
+ } else if (tex.source instanceof HTMLCanvasElement) {
277
+ uploadElement(gl, tex.texture, tex.unit, tex.source);
278
+ tex.width = tex.source.width;
279
+ tex.height = tex.source.height;
280
+ }
281
+ }
173
282
  }
174
283
  function bindTextures(gl, locations, textures) {
175
284
  for (let i = 0; i < 4; i++) {
@@ -187,9 +296,6 @@ function disposeTextures(gl, textures) {
187
296
  if (tex) gl.deleteTexture(tex.texture);
188
297
  }
189
298
  }
190
- function isPOT(v) {
191
- return (v & v - 1) === 0 && v > 0;
192
- }
193
299
 
194
300
  // src/uniforms.ts
195
301
  function updateUniforms(state, delta, speed, mouse) {
@@ -286,10 +392,10 @@ function useShadertoy({
286
392
  if (texturesProp) {
287
393
  for (let i = 0; i < 4; i++) {
288
394
  const src = texturesProp[CHANNEL_KEYS[i]];
289
- if (typeof src === "string") {
290
- const { state, promise } = loadImageTexture(result.gl, src, i);
395
+ if (src != null) {
396
+ const { state, promise } = createTexture(result.gl, src, i);
291
397
  result.textures[i] = state;
292
- texturePromises.push(promise);
398
+ if (promise) texturePromises.push(promise);
293
399
  }
294
400
  }
295
401
  }
@@ -315,6 +421,7 @@ function useShadertoy({
315
421
  lastTimestamp = timestamp;
316
422
  if (!pausedRef.current && rendererRef.current) {
317
423
  const r = rendererRef.current;
424
+ updateDynamicTextures(r.gl, r.textures);
318
425
  bindTextures(r.gl, r.locations.iChannel, r.textures);
319
426
  updateUniforms(r, delta, speedRef.current, mouseState.current);
320
427
  render(r);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-shadertoy",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Run Shadertoy GLSL shaders in React. Copy-paste and it works.",
5
5
  "author": "Wrennly (https://github.com/wrennly)",
6
6
  "license": "MIT",