@fjandin/react-shader 0.0.9 → 0.0.10

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 CHANGED
@@ -48,8 +48,7 @@ function App() {
48
48
  | `fragment` | `string` | Yes | - | GLSL fragment shader source code |
49
49
  | `vertex` | `string` | No | Default quad shader | GLSL vertex shader source code |
50
50
  | `uniforms` | `Record<string, UniformValue>` | No | `{}` | Custom uniform values |
51
- | `className` | `string` | No | - | CSS class name for the container |
52
- | `debug` | `boolean` | No | `false` | Show debug overlay with resolution and mouse info |
51
+ | `className` | `string` | No | - | CSS class name for the canvas |
53
52
  | `fullscreen` | `boolean` | No | `false` | Render as fixed fullscreen overlay |
54
53
  | `timeScale` | `number` | No | `1` | Scale factor for elapsed time |
55
54
  | `onFrame` | `(info: FrameInfo) => void` | No | - | Callback invoked on each frame |
@@ -175,6 +174,7 @@ The `FrameInfo` object contains:
175
174
  - `time` - Total elapsed time in seconds
176
175
  - `resolution` - Canvas resolution as `[width, height]`
177
176
  - `mouse` - Mouse position as `[x, y]`
177
+ - `mouseNormalized` - Aspect-corrected mouse position as `[x, y]`
178
178
  - `mouseLeftDown` - Whether left mouse button is pressed
179
179
 
180
180
  ## TypeScript
@@ -215,11 +215,12 @@ const declarations = generateUniformDeclarations({
215
215
  ## Features
216
216
 
217
217
  - WebGL2 with WebGL1 fallback
218
- - High-DPI display support via `devicePixelRatio`
218
+ - High-DPI display support with automatic DPR change detection
219
219
  - Automatic canvas resizing
220
220
  - Shader compilation error display
221
221
  - Context loss/restoration handling
222
222
  - Mouse tracking with WebGL coordinate convention
223
+ - Optimized render loop with minimal per-frame allocations
223
224
 
224
225
  ## Requirements
225
226
 
@@ -1 +1 @@
1
- {"version":3,"file":"ReactShader.d.ts","sourceRoot":"","sources":["../src/ReactShader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAa,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAU1D,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,QAAQ,EACR,MAAuB,EACvB,QAAQ,EACR,UAAkB,EAClB,SAAa,EACb,OAAO,EACP,OAAO,EACP,WAAW,GACZ,EAAE,gBAAgB,2CA0ElB"}
1
+ {"version":3,"file":"ReactShader.d.ts","sourceRoot":"","sources":["../src/ReactShader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAgC/C,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,QAAQ,EACR,MAAuB,EACvB,QAAQ,EACR,UAAkB,EAClB,SAAa,EACb,OAAO,EACP,OAAO,EACP,WAAW,GACZ,EAAE,gBAAgB,2CAuDlB"}
@@ -7,7 +7,6 @@ interface UseWebGLOptions {
7
7
  onFrame?: (info: FrameInfo) => void;
8
8
  onClick?: (info: FrameInfo) => void;
9
9
  onMouseMove?: (info: FrameInfo) => void;
10
- running?: boolean;
11
10
  timeScale?: number;
12
11
  }
13
12
  export declare function useWebGL(options: UseWebGLOptions): {
@@ -1 +1 @@
1
- {"version":3,"file":"useWebGL.d.ts","sourceRoot":"","sources":["../../src/hooks/useWebGL.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAIvD,UAAU,eAAe;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACvC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAChC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACvC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAqED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe;;;EA+PhD"}
1
+ {"version":3,"file":"useWebGL.d.ts","sourceRoot":"","sources":["../../src/hooks/useWebGL.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAIvD,UAAU,eAAe;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACvC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAChC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACvC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAqED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe;;;EAmQhD"}
package/dist/index.cjs CHANGED
@@ -273,6 +273,14 @@ function useWebGL(options) {
273
273
  const timeScaleRef = import_react.useRef(options.timeScale ?? 1);
274
274
  const vertexRef = import_react.useRef(options.vertex);
275
275
  const fragmentRef = import_react.useRef(options.fragment);
276
+ const dprRef = import_react.useRef(window.devicePixelRatio || 1);
277
+ const defaultUniformsRef = import_react.useRef({
278
+ iTime: 0,
279
+ iMouse: [0, 0],
280
+ iMouseNormalized: [0, 0],
281
+ iMouseLeftDown: 0,
282
+ iResolution: [0, 0]
283
+ });
276
284
  uniformsRef.current = options.uniforms;
277
285
  onErrorRef.current = options.onError;
278
286
  onFrameRef.current = options.onFrame;
@@ -290,10 +298,9 @@ function useWebGL(options) {
290
298
  return;
291
299
  const deltaTime = lastFrameTimeRef.current === 0 ? 0 : (time - lastFrameTimeRef.current) / 1000;
292
300
  lastFrameTimeRef.current = time;
293
- elapsedTimeRef.current += deltaTime * timeScaleRef.current;
294
301
  const { gl, program, positionAttributeLocation, uniformLocationCache } = state;
295
302
  const elapsedTime = elapsedTimeRef.current;
296
- const dpr = window.devicePixelRatio || 1;
303
+ const dpr = dprRef.current;
297
304
  const displayWidth = canvas.clientWidth;
298
305
  const displayHeight = canvas.clientHeight;
299
306
  if (displayWidth === 0 || displayHeight === 0) {
@@ -313,22 +320,13 @@ function useWebGL(options) {
313
320
  gl.enableVertexAttribArray(positionAttributeLocation);
314
321
  gl.bindBuffer(gl.ARRAY_BUFFER, state.positionBuffer);
315
322
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
316
- const minDimension = Math.min(canvas.width, canvas.height) || 1;
317
- mouseNormalizedRef.current = [
318
- (mouseRef.current[0] - canvas.width / 2) / minDimension,
319
- (mouseRef.current[1] - canvas.height / 2) / minDimension
320
- ];
321
- const defaultUniforms = {
322
- iTime: elapsedTime,
323
- iMouse: mouseRef.current,
324
- iMouseNormalized: mouseNormalizedRef.current,
325
- iMouseLeftDown: mouseLeftDownRef.current ? 1 : 0,
326
- iResolution: [canvas.width, canvas.height]
327
- };
328
- setUniforms(gl, program, defaultUniforms, uniformLocationCache);
329
- if (uniformsRef.current) {
330
- setUniforms(gl, program, uniformsRef.current, uniformLocationCache);
331
- }
323
+ const defaultUniforms = defaultUniformsRef.current;
324
+ defaultUniforms.iTime = elapsedTime;
325
+ defaultUniforms.iMouse = mouseRef.current;
326
+ defaultUniforms.iMouseNormalized = mouseNormalizedRef.current;
327
+ defaultUniforms.iMouseLeftDown = mouseLeftDownRef.current ? 1 : 0;
328
+ defaultUniforms.iResolution = [canvas.width, canvas.height];
329
+ setUniforms(gl, program, { ...defaultUniforms, ...uniformsRef.current }, uniformLocationCache);
332
330
  gl.drawArrays(gl.TRIANGLES, 0, 6);
333
331
  if (onFrameRef.current) {
334
332
  onFrameRef.current({
@@ -371,10 +369,16 @@ function useWebGL(options) {
371
369
  const handleContextRestored = () => {
372
370
  initialize();
373
371
  };
372
+ const dprMediaQuery = window.matchMedia(`(resolution: ${dprRef.current}dppx)`);
373
+ const handleDprChange = () => {
374
+ dprRef.current = window.devicePixelRatio || 1;
375
+ };
376
+ dprMediaQuery.addEventListener("change", handleDprChange);
374
377
  canvas.addEventListener("webglcontextlost", handleContextLost);
375
378
  canvas.addEventListener("webglcontextrestored", handleContextRestored);
376
379
  initialize();
377
380
  return () => {
381
+ dprMediaQuery.removeEventListener("change", handleDprChange);
378
382
  canvas.removeEventListener("webglcontextlost", handleContextLost);
379
383
  canvas.removeEventListener("webglcontextrestored", handleContextRestored);
380
384
  cancelAnimationFrame(animationFrameRef.current);
@@ -399,7 +403,7 @@ function useWebGL(options) {
399
403
  const rect = canvasRectRef.current;
400
404
  if (!rect)
401
405
  return;
402
- const dpr = window.devicePixelRatio || 1;
406
+ const dpr = dprRef.current;
403
407
  const x = (event.clientX - rect.left) * dpr;
404
408
  const y = (rect.height - (event.clientY - rect.top)) * dpr;
405
409
  mouseRef.current = [x, y];
@@ -464,6 +468,24 @@ void main() {
464
468
  gl_Position = vec4(a_position, 0.0, 1.0);
465
469
  }
466
470
  `;
471
+ var FULLSCREEN_CONTAINER_STYLE = {
472
+ position: "fixed",
473
+ top: 0,
474
+ left: 0,
475
+ width: "100vw",
476
+ height: "100vh",
477
+ zIndex: 9000
478
+ };
479
+ var DEFAULT_CONTAINER_STYLE = {
480
+ position: "relative",
481
+ width: "100%",
482
+ height: "100%"
483
+ };
484
+ var CANVAS_STYLE = {
485
+ display: "block",
486
+ width: "100%",
487
+ height: "100%"
488
+ };
467
489
  function ReactShader({
468
490
  className,
469
491
  fragment,
@@ -480,11 +502,6 @@ function ReactShader({
480
502
  setError(err.message);
481
503
  console.error("ReactShader error:", err);
482
504
  }, []);
483
- const handleFrame = import_react2.useCallback((info) => {
484
- if (onFrame) {
485
- onFrame(info);
486
- }
487
- }, [onFrame]);
488
505
  import_react2.useEffect(() => {
489
506
  setError(null);
490
507
  }, [fragment, vertex]);
@@ -493,23 +510,12 @@ function ReactShader({
493
510
  vertex,
494
511
  uniforms,
495
512
  onError: handleError,
496
- onFrame: handleFrame,
513
+ onFrame,
497
514
  onClick,
498
515
  onMouseMove,
499
516
  timeScale
500
517
  });
501
- const containerStyle = fullscreen ? {
502
- position: "fixed",
503
- top: 0,
504
- left: 0,
505
- width: "100vw",
506
- height: "100vh",
507
- zIndex: 9000
508
- } : {
509
- position: "relative",
510
- width: "100%",
511
- height: "100%"
512
- };
518
+ const containerStyle = import_react2.useMemo(() => fullscreen ? FULLSCREEN_CONTAINER_STYLE : DEFAULT_CONTAINER_STYLE, [fullscreen]);
513
519
  if (error) {
514
520
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
515
521
  className,
@@ -537,7 +543,7 @@ function ReactShader({
537
543
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("canvas", {
538
544
  ref: canvasRef,
539
545
  className,
540
- style: { display: "block", width: "100%", height: "100%" }
546
+ style: CANVAS_STYLE
541
547
  }, undefined, false, undefined, this);
542
548
  }
543
549
  // src/shaders/color-palette.ts
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/ReactShader.tsx
2
- import { useCallback as useCallback2, useEffect as useEffect2, useState } from "react";
2
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useState } from "react";
3
3
 
4
4
  // src/hooks/useWebGL.ts
5
5
  import { useCallback, useEffect, useRef } from "react";
@@ -233,6 +233,14 @@ function useWebGL(options) {
233
233
  const timeScaleRef = useRef(options.timeScale ?? 1);
234
234
  const vertexRef = useRef(options.vertex);
235
235
  const fragmentRef = useRef(options.fragment);
236
+ const dprRef = useRef(window.devicePixelRatio || 1);
237
+ const defaultUniformsRef = useRef({
238
+ iTime: 0,
239
+ iMouse: [0, 0],
240
+ iMouseNormalized: [0, 0],
241
+ iMouseLeftDown: 0,
242
+ iResolution: [0, 0]
243
+ });
236
244
  uniformsRef.current = options.uniforms;
237
245
  onErrorRef.current = options.onError;
238
246
  onFrameRef.current = options.onFrame;
@@ -250,10 +258,9 @@ function useWebGL(options) {
250
258
  return;
251
259
  const deltaTime = lastFrameTimeRef.current === 0 ? 0 : (time - lastFrameTimeRef.current) / 1000;
252
260
  lastFrameTimeRef.current = time;
253
- elapsedTimeRef.current += deltaTime * timeScaleRef.current;
254
261
  const { gl, program, positionAttributeLocation, uniformLocationCache } = state;
255
262
  const elapsedTime = elapsedTimeRef.current;
256
- const dpr = window.devicePixelRatio || 1;
263
+ const dpr = dprRef.current;
257
264
  const displayWidth = canvas.clientWidth;
258
265
  const displayHeight = canvas.clientHeight;
259
266
  if (displayWidth === 0 || displayHeight === 0) {
@@ -273,22 +280,13 @@ function useWebGL(options) {
273
280
  gl.enableVertexAttribArray(positionAttributeLocation);
274
281
  gl.bindBuffer(gl.ARRAY_BUFFER, state.positionBuffer);
275
282
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
276
- const minDimension = Math.min(canvas.width, canvas.height) || 1;
277
- mouseNormalizedRef.current = [
278
- (mouseRef.current[0] - canvas.width / 2) / minDimension,
279
- (mouseRef.current[1] - canvas.height / 2) / minDimension
280
- ];
281
- const defaultUniforms = {
282
- iTime: elapsedTime,
283
- iMouse: mouseRef.current,
284
- iMouseNormalized: mouseNormalizedRef.current,
285
- iMouseLeftDown: mouseLeftDownRef.current ? 1 : 0,
286
- iResolution: [canvas.width, canvas.height]
287
- };
288
- setUniforms(gl, program, defaultUniforms, uniformLocationCache);
289
- if (uniformsRef.current) {
290
- setUniforms(gl, program, uniformsRef.current, uniformLocationCache);
291
- }
283
+ const defaultUniforms = defaultUniformsRef.current;
284
+ defaultUniforms.iTime = elapsedTime;
285
+ defaultUniforms.iMouse = mouseRef.current;
286
+ defaultUniforms.iMouseNormalized = mouseNormalizedRef.current;
287
+ defaultUniforms.iMouseLeftDown = mouseLeftDownRef.current ? 1 : 0;
288
+ defaultUniforms.iResolution = [canvas.width, canvas.height];
289
+ setUniforms(gl, program, { ...defaultUniforms, ...uniformsRef.current }, uniformLocationCache);
292
290
  gl.drawArrays(gl.TRIANGLES, 0, 6);
293
291
  if (onFrameRef.current) {
294
292
  onFrameRef.current({
@@ -331,10 +329,16 @@ function useWebGL(options) {
331
329
  const handleContextRestored = () => {
332
330
  initialize();
333
331
  };
332
+ const dprMediaQuery = window.matchMedia(`(resolution: ${dprRef.current}dppx)`);
333
+ const handleDprChange = () => {
334
+ dprRef.current = window.devicePixelRatio || 1;
335
+ };
336
+ dprMediaQuery.addEventListener("change", handleDprChange);
334
337
  canvas.addEventListener("webglcontextlost", handleContextLost);
335
338
  canvas.addEventListener("webglcontextrestored", handleContextRestored);
336
339
  initialize();
337
340
  return () => {
341
+ dprMediaQuery.removeEventListener("change", handleDprChange);
338
342
  canvas.removeEventListener("webglcontextlost", handleContextLost);
339
343
  canvas.removeEventListener("webglcontextrestored", handleContextRestored);
340
344
  cancelAnimationFrame(animationFrameRef.current);
@@ -359,7 +363,7 @@ function useWebGL(options) {
359
363
  const rect = canvasRectRef.current;
360
364
  if (!rect)
361
365
  return;
362
- const dpr = window.devicePixelRatio || 1;
366
+ const dpr = dprRef.current;
363
367
  const x = (event.clientX - rect.left) * dpr;
364
368
  const y = (rect.height - (event.clientY - rect.top)) * dpr;
365
369
  mouseRef.current = [x, y];
@@ -424,6 +428,24 @@ void main() {
424
428
  gl_Position = vec4(a_position, 0.0, 1.0);
425
429
  }
426
430
  `;
431
+ var FULLSCREEN_CONTAINER_STYLE = {
432
+ position: "fixed",
433
+ top: 0,
434
+ left: 0,
435
+ width: "100vw",
436
+ height: "100vh",
437
+ zIndex: 9000
438
+ };
439
+ var DEFAULT_CONTAINER_STYLE = {
440
+ position: "relative",
441
+ width: "100%",
442
+ height: "100%"
443
+ };
444
+ var CANVAS_STYLE = {
445
+ display: "block",
446
+ width: "100%",
447
+ height: "100%"
448
+ };
427
449
  function ReactShader({
428
450
  className,
429
451
  fragment,
@@ -440,11 +462,6 @@ function ReactShader({
440
462
  setError(err.message);
441
463
  console.error("ReactShader error:", err);
442
464
  }, []);
443
- const handleFrame = useCallback2((info) => {
444
- if (onFrame) {
445
- onFrame(info);
446
- }
447
- }, [onFrame]);
448
465
  useEffect2(() => {
449
466
  setError(null);
450
467
  }, [fragment, vertex]);
@@ -453,23 +470,12 @@ function ReactShader({
453
470
  vertex,
454
471
  uniforms,
455
472
  onError: handleError,
456
- onFrame: handleFrame,
473
+ onFrame,
457
474
  onClick,
458
475
  onMouseMove,
459
476
  timeScale
460
477
  });
461
- const containerStyle = fullscreen ? {
462
- position: "fixed",
463
- top: 0,
464
- left: 0,
465
- width: "100vw",
466
- height: "100vh",
467
- zIndex: 9000
468
- } : {
469
- position: "relative",
470
- width: "100%",
471
- height: "100%"
472
- };
478
+ const containerStyle = useMemo(() => fullscreen ? FULLSCREEN_CONTAINER_STYLE : DEFAULT_CONTAINER_STYLE, [fullscreen]);
473
479
  if (error) {
474
480
  return /* @__PURE__ */ jsxDEV("div", {
475
481
  className,
@@ -497,7 +503,7 @@ function ReactShader({
497
503
  return /* @__PURE__ */ jsxDEV("canvas", {
498
504
  ref: canvasRef,
499
505
  className,
500
- style: { display: "block", width: "100%", height: "100%" }
506
+ style: CANVAS_STYLE
501
507
  }, undefined, false, undefined, this);
502
508
  }
503
509
  // src/shaders/color-palette.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjandin/react-shader",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "React component for rendering WebGL shaders",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",