@fjandin/react-shader 0.0.8 → 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"}
@@ -1 +1 @@
1
- {"version":3,"file":"shader.d.ts","sourceRoot":"","sources":["../../src/example/shader.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,QA4FnB,CAAA"}
1
+ {"version":3,"file":"shader.d.ts","sourceRoot":"","sources":["../../src/example/shader.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,QAAQ,QAwCnB,CAAA"}
@@ -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
@@ -29,7 +29,10 @@ var __export = (target, all) => {
29
29
  // src/index.ts
30
30
  var exports_src = {};
31
31
  __export(exports_src, {
32
+ generateUtilsFunction: () => generateUtilsFunction,
32
33
  generateSimplexNoiseFunction: () => generateSimplexNoiseFunction,
34
+ generateSceneCirclesFunction: () => generateSceneCirclesFunction,
35
+ generateDistortionRippleFunction: () => generateDistortionRippleFunction,
33
36
  generateColorPaletteFunction: () => generateColorPaletteFunction,
34
37
  ReactShader: () => ReactShader
35
38
  });
@@ -270,6 +273,14 @@ function useWebGL(options) {
270
273
  const timeScaleRef = import_react.useRef(options.timeScale ?? 1);
271
274
  const vertexRef = import_react.useRef(options.vertex);
272
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
+ });
273
284
  uniformsRef.current = options.uniforms;
274
285
  onErrorRef.current = options.onError;
275
286
  onFrameRef.current = options.onFrame;
@@ -287,10 +298,9 @@ function useWebGL(options) {
287
298
  return;
288
299
  const deltaTime = lastFrameTimeRef.current === 0 ? 0 : (time - lastFrameTimeRef.current) / 1000;
289
300
  lastFrameTimeRef.current = time;
290
- elapsedTimeRef.current += deltaTime * timeScaleRef.current;
291
301
  const { gl, program, positionAttributeLocation, uniformLocationCache } = state;
292
302
  const elapsedTime = elapsedTimeRef.current;
293
- const dpr = window.devicePixelRatio || 1;
303
+ const dpr = dprRef.current;
294
304
  const displayWidth = canvas.clientWidth;
295
305
  const displayHeight = canvas.clientHeight;
296
306
  if (displayWidth === 0 || displayHeight === 0) {
@@ -310,22 +320,13 @@ function useWebGL(options) {
310
320
  gl.enableVertexAttribArray(positionAttributeLocation);
311
321
  gl.bindBuffer(gl.ARRAY_BUFFER, state.positionBuffer);
312
322
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
313
- const minDimension = Math.min(canvas.width, canvas.height) || 1;
314
- mouseNormalizedRef.current = [
315
- (mouseRef.current[0] - canvas.width / 2) / minDimension,
316
- (mouseRef.current[1] - canvas.height / 2) / minDimension
317
- ];
318
- const defaultUniforms = {
319
- iTime: elapsedTime,
320
- iMouse: mouseRef.current,
321
- iMouseNormalized: mouseNormalizedRef.current,
322
- iMouseLeftDown: mouseLeftDownRef.current ? 1 : 0,
323
- iResolution: [canvas.width, canvas.height]
324
- };
325
- setUniforms(gl, program, defaultUniforms, uniformLocationCache);
326
- if (uniformsRef.current) {
327
- setUniforms(gl, program, uniformsRef.current, uniformLocationCache);
328
- }
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);
329
330
  gl.drawArrays(gl.TRIANGLES, 0, 6);
330
331
  if (onFrameRef.current) {
331
332
  onFrameRef.current({
@@ -368,10 +369,16 @@ function useWebGL(options) {
368
369
  const handleContextRestored = () => {
369
370
  initialize();
370
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);
371
377
  canvas.addEventListener("webglcontextlost", handleContextLost);
372
378
  canvas.addEventListener("webglcontextrestored", handleContextRestored);
373
379
  initialize();
374
380
  return () => {
381
+ dprMediaQuery.removeEventListener("change", handleDprChange);
375
382
  canvas.removeEventListener("webglcontextlost", handleContextLost);
376
383
  canvas.removeEventListener("webglcontextrestored", handleContextRestored);
377
384
  cancelAnimationFrame(animationFrameRef.current);
@@ -396,7 +403,7 @@ function useWebGL(options) {
396
403
  const rect = canvasRectRef.current;
397
404
  if (!rect)
398
405
  return;
399
- const dpr = window.devicePixelRatio || 1;
406
+ const dpr = dprRef.current;
400
407
  const x = (event.clientX - rect.left) * dpr;
401
408
  const y = (rect.height - (event.clientY - rect.top)) * dpr;
402
409
  mouseRef.current = [x, y];
@@ -461,6 +468,24 @@ void main() {
461
468
  gl_Position = vec4(a_position, 0.0, 1.0);
462
469
  }
463
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
+ };
464
489
  function ReactShader({
465
490
  className,
466
491
  fragment,
@@ -477,11 +502,6 @@ function ReactShader({
477
502
  setError(err.message);
478
503
  console.error("ReactShader error:", err);
479
504
  }, []);
480
- const handleFrame = import_react2.useCallback((info) => {
481
- if (onFrame) {
482
- onFrame(info);
483
- }
484
- }, [onFrame]);
485
505
  import_react2.useEffect(() => {
486
506
  setError(null);
487
507
  }, [fragment, vertex]);
@@ -490,23 +510,12 @@ function ReactShader({
490
510
  vertex,
491
511
  uniforms,
492
512
  onError: handleError,
493
- onFrame: handleFrame,
513
+ onFrame,
494
514
  onClick,
495
515
  onMouseMove,
496
516
  timeScale
497
517
  });
498
- const containerStyle = fullscreen ? {
499
- position: "fixed",
500
- top: 0,
501
- left: 0,
502
- width: "100vw",
503
- height: "100vh",
504
- zIndex: 9000
505
- } : {
506
- position: "relative",
507
- width: "100%",
508
- height: "100%"
509
- };
518
+ const containerStyle = import_react2.useMemo(() => fullscreen ? FULLSCREEN_CONTAINER_STYLE : DEFAULT_CONTAINER_STYLE, [fullscreen]);
510
519
  if (error) {
511
520
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
512
521
  className,
@@ -534,7 +543,7 @@ function ReactShader({
534
543
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("canvas", {
535
544
  ref: canvasRef,
536
545
  className,
537
- style: { display: "block", width: "100%", height: "100%" }
546
+ style: CANVAS_STYLE
538
547
  }, undefined, false, undefined, this);
539
548
  }
540
549
  // src/shaders/color-palette.ts
@@ -550,6 +559,63 @@ function generateColorPaletteFunction(name, paletteString) {
550
559
  }
551
560
  `;
552
561
  }
562
+ // src/shaders/distortion-ripple.ts
563
+ function generateDistortionRippleFunction() {
564
+ return `
565
+ vec2 DistortionRipple(vec2 uv, vec2 center, float radius, float intensity, float thickness) {
566
+ // 1. Calculate vector and distance from center
567
+ vec2 dir = uv - center;
568
+ float dist = length(dir);
569
+
570
+ // 2. Create a mask so the ripple only exists near the radius Z
571
+ // Using smoothstep creates a soft edge for the ripple
572
+ float mask = smoothstep(radius + thickness, radius, dist) * smoothstep(radius - thickness, radius, dist);
573
+
574
+ // 3. Calculate the displacement amount using a Sine wave
575
+ // We subtract dist from radius to orient the wave correctly
576
+ float wave = sin((dist - radius) * 20.0);
577
+
578
+ // 4. Apply intensity and mask, then offset the UV
579
+ vec2 offset = normalize(dir) * wave * intensity * mask;
580
+
581
+ return offset;
582
+ }
583
+ `;
584
+ }
585
+ // src/shaders/scene-circles.ts
586
+ function generateSceneCirclesFunction(paletteString = "[[0.5 0.5 0.5] [0.5 0.5 0.5] [1.0 1.0 1.0] [0.263 0.416 0.557]]") {
587
+ return `
588
+ ${generateColorPaletteFunction("circlesPalette", paletteString)}
589
+
590
+ vec3 SceneCircles(
591
+ vec2 uv0,
592
+ float iterations,
593
+ float fractMultiplier,
594
+ float time,
595
+ float waveLength,
596
+ float edgeBlur,
597
+ float contrast
598
+ ) {
599
+ vec3 col = vec3(0.0);
600
+ vec2 uv = uv0;
601
+
602
+ for (float i = 0.0; i < iterations; i++) {
603
+ uv = fract(uv * fractMultiplier) - 0.5;
604
+
605
+ float d = length(uv) * exp(-length(uv0));
606
+
607
+ vec3 color = circlesPalette(length(uv0) + i * 0.4 + time * 0.4);
608
+
609
+ d = sin(d * waveLength + time) / waveLength;
610
+ d = abs(d);
611
+ d = pow(edgeBlur / d, contrast);
612
+
613
+ col += color * d;
614
+ }
615
+ return col;
616
+ }
617
+ `;
618
+ }
553
619
  // src/shaders/simplex-noise.ts
554
620
  function generateSimplexNoiseFunction() {
555
621
  return `
@@ -734,3 +800,15 @@ function generateSimplexNoiseFunction() {
734
800
  }
735
801
  `;
736
802
  }
803
+ // src/shaders/utils.ts
804
+ function generateUtilsFunction() {
805
+ return `
806
+ vec2 GetUv(vec2 fragCoord, vec2 resolution) {
807
+ return (fragCoord - 0.5 * resolution) / resolution.y;
808
+ }
809
+
810
+ vec2 GetMouse(vec2 mouse, vec2 resolution) {
811
+ return (mouse - 0.5 * resolution) / resolution.y;
812
+ }
813
+ `;
814
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { ReactShader } from "./ReactShader";
2
2
  export { generateColorPaletteFunction } from "./shaders/color-palette";
3
+ export { generateDistortionRippleFunction } from "./shaders/distortion-ripple";
4
+ export { generateSceneCirclesFunction } from "./shaders/scene-circles";
3
5
  export { generateSimplexNoiseFunction } from "./shaders/simplex-noise";
6
+ export { generateUtilsFunction } from "./shaders/utils";
4
7
  export type { DefaultUniforms, FloatArray, FrameInfo, ReactShaderProps, UniformValue, Vec2, Vec2Array, Vec3, Vec3Array, Vec4, Vec4Array, } from "./types";
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AACtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AACtE,YAAY,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,IAAI,EACJ,SAAS,EACT,IAAI,EACJ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AACtE,OAAO,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAA;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AACtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AACvD,YAAY,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,IAAI,EACJ,SAAS,EACT,IAAI,EACJ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,MAAM,SAAS,CAAA"}
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
@@ -513,6 +519,63 @@ function generateColorPaletteFunction(name, paletteString) {
513
519
  }
514
520
  `;
515
521
  }
522
+ // src/shaders/distortion-ripple.ts
523
+ function generateDistortionRippleFunction() {
524
+ return `
525
+ vec2 DistortionRipple(vec2 uv, vec2 center, float radius, float intensity, float thickness) {
526
+ // 1. Calculate vector and distance from center
527
+ vec2 dir = uv - center;
528
+ float dist = length(dir);
529
+
530
+ // 2. Create a mask so the ripple only exists near the radius Z
531
+ // Using smoothstep creates a soft edge for the ripple
532
+ float mask = smoothstep(radius + thickness, radius, dist) * smoothstep(radius - thickness, radius, dist);
533
+
534
+ // 3. Calculate the displacement amount using a Sine wave
535
+ // We subtract dist from radius to orient the wave correctly
536
+ float wave = sin((dist - radius) * 20.0);
537
+
538
+ // 4. Apply intensity and mask, then offset the UV
539
+ vec2 offset = normalize(dir) * wave * intensity * mask;
540
+
541
+ return offset;
542
+ }
543
+ `;
544
+ }
545
+ // src/shaders/scene-circles.ts
546
+ function generateSceneCirclesFunction(paletteString = "[[0.5 0.5 0.5] [0.5 0.5 0.5] [1.0 1.0 1.0] [0.263 0.416 0.557]]") {
547
+ return `
548
+ ${generateColorPaletteFunction("circlesPalette", paletteString)}
549
+
550
+ vec3 SceneCircles(
551
+ vec2 uv0,
552
+ float iterations,
553
+ float fractMultiplier,
554
+ float time,
555
+ float waveLength,
556
+ float edgeBlur,
557
+ float contrast
558
+ ) {
559
+ vec3 col = vec3(0.0);
560
+ vec2 uv = uv0;
561
+
562
+ for (float i = 0.0; i < iterations; i++) {
563
+ uv = fract(uv * fractMultiplier) - 0.5;
564
+
565
+ float d = length(uv) * exp(-length(uv0));
566
+
567
+ vec3 color = circlesPalette(length(uv0) + i * 0.4 + time * 0.4);
568
+
569
+ d = sin(d * waveLength + time) / waveLength;
570
+ d = abs(d);
571
+ d = pow(edgeBlur / d, contrast);
572
+
573
+ col += color * d;
574
+ }
575
+ return col;
576
+ }
577
+ `;
578
+ }
516
579
  // src/shaders/simplex-noise.ts
517
580
  function generateSimplexNoiseFunction() {
518
581
  return `
@@ -697,8 +760,23 @@ function generateSimplexNoiseFunction() {
697
760
  }
698
761
  `;
699
762
  }
763
+ // src/shaders/utils.ts
764
+ function generateUtilsFunction() {
765
+ return `
766
+ vec2 GetUv(vec2 fragCoord, vec2 resolution) {
767
+ return (fragCoord - 0.5 * resolution) / resolution.y;
768
+ }
769
+
770
+ vec2 GetMouse(vec2 mouse, vec2 resolution) {
771
+ return (mouse - 0.5 * resolution) / resolution.y;
772
+ }
773
+ `;
774
+ }
700
775
  export {
776
+ generateUtilsFunction,
701
777
  generateSimplexNoiseFunction,
778
+ generateSceneCirclesFunction,
779
+ generateDistortionRippleFunction,
702
780
  generateColorPaletteFunction,
703
781
  ReactShader
704
782
  };
@@ -0,0 +1,16 @@
1
+ /**
2
+ *
3
+ * @returns glsl function for distortion ripple
4
+ *
5
+ * vec2 uv: uv coordinates
6
+ *
7
+ * vec2 center: center of the ripple
8
+ *
9
+ * float radius: radius of the ripple
10
+ *
11
+ * float intensity: intensity of the ripple
12
+ *
13
+ * float thickness: thickness of the ripple
14
+ */
15
+ export declare function generateDistortionRippleFunction(): string;
16
+ //# sourceMappingURL=distortion-ripple.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distortion-ripple.d.ts","sourceRoot":"","sources":["../../src/shaders/distortion-ripple.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gCAAgC,WAqB/C"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ *
3
+ * @param paletteString
4
+ * @returns glsl function for rendering circles
5
+ *
6
+ * vec2 uv0: uv coordinates
7
+ *
8
+ * float iterations: number of iterations
9
+ *
10
+ * float fractMultiplier: fract multiplier
11
+ *
12
+ * float time: time
13
+ *
14
+ * float waveLength: wave length
15
+ *
16
+ * float edgeBlur: edge blur
17
+ *
18
+ * float contrast: contrast
19
+ */
20
+ export declare function generateSceneCirclesFunction(paletteString?: string): string;
21
+ //# sourceMappingURL=scene-circles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-circles.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-circles.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,CAC1C,aAAa,SAAoE,UAiClF"}
@@ -0,0 +1,2 @@
1
+ export declare function generateUtilsFunction(): string;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shaders/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,qBAAqB,WAUpC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjandin/react-shader",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "React component for rendering WebGL shaders",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",