diff --git a/public/r/Grainient-JS-CSS.json b/public/r/Grainient-JS-CSS.json
index 5a067d81..0aa5382a 100644
--- a/public/r/Grainient-JS-CSS.json
+++ b/public/r/Grainient-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "Grainient/Grainient.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Grainient.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\nconst Grainient = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n\n const container = containerRef.current;\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: timeSpeed },\n uColorBalance: { value: colorBalance },\n uWarpStrength: { value: warpStrength },\n uWarpFrequency: { value: warpFrequency },\n uWarpSpeed: { value: warpSpeed },\n uWarpAmplitude: { value: warpAmplitude },\n uBlendAngle: { value: blendAngle },\n uBlendSoftness: { value: blendSoftness },\n uRotationAmount: { value: rotationAmount },\n uNoiseScale: { value: noiseScale },\n uGrainAmount: { value: grainAmount },\n uGrainScale: { value: grainScale },\n uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },\n uContrast: { value: contrast },\n uGamma: { value: gamma },\n uSaturation: { value: saturation },\n uCenterOffset: { value: new Float32Array([centerX, centerY]) },\n uZoom: { value: zoom },\n uColor1: { value: new Float32Array(hexToRgb(color1)) },\n uColor2: { value: new Float32Array(hexToRgb(color2)) },\n uColor3: { value: new Float32Array(hexToRgb(color3)) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width));\n const height = Math.max(1, Math.floor(rect.height));\n renderer.setSize(width, height);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n const t0 = performance.now();\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n try {\n container.removeChild(canvas);\n } catch {\n // Ignore\n }\n };\n }, [\n timeSpeed,\n colorBalance,\n warpStrength,\n warpFrequency,\n warpSpeed,\n warpAmplitude,\n blendAngle,\n blendSoftness,\n rotationAmount,\n noiseScale,\n grainAmount,\n grainScale,\n grainAnimated,\n contrast,\n gamma,\n saturation,\n centerX,\n centerY,\n zoom,\n color1,\n color2,\n color3\n ]);\n\n return
;\n};\n\nexport default Grainient;\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Grainient.css';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\n\n// Keep renderer/program alive across re-renders so Effect 2 can update\n// uniforms without ever rebuilding the WebGL context.\nconst ctxMap = new WeakMap();\n\nconst Grainient = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n // Effect 1: build WebGL context once, pause when offscreen / tab hidden\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: 0.25 },\n uColorBalance: { value: 0.0 },\n uWarpStrength: { value: 1.0 },\n uWarpFrequency: { value: 5.0 },\n uWarpSpeed: { value: 2.0 },\n uWarpAmplitude: { value: 50.0 },\n uBlendAngle: { value: 0.0 },\n uBlendSoftness: { value: 0.05 },\n uRotationAmount: { value: 500.0 },\n uNoiseScale: { value: 2.0 },\n uGrainAmount: { value: 0.1 },\n uGrainScale: { value: 2.0 },\n uGrainAnimated: { value: 0.0 },\n uContrast: { value: 1.5 },\n uGamma: { value: 1.0 },\n uSaturation: { value: 1.0 },\n uCenterOffset: { value: new Float32Array([0, 0]) },\n uZoom: { value: 0.9 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n ctxMap.delete(container);\n try { container.removeChild(canvas); } catch { /* ignore */ }\n };\n }, []); // renderer created once\n\n // Effect 2: sync props to uniforms — zero GPU cost, no teardown\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms;\n\n u.uTimeSpeed.value = timeSpeed;\n u.uColorBalance.value = colorBalance;\n u.uWarpStrength.value = warpStrength;\n u.uWarpFrequency.value = warpFrequency;\n u.uWarpSpeed.value = warpSpeed;\n u.uWarpAmplitude.value = warpAmplitude;\n u.uBlendAngle.value = blendAngle;\n u.uBlendSoftness.value = blendSoftness;\n u.uRotationAmount.value = rotationAmount;\n u.uNoiseScale.value = noiseScale;\n u.uGrainAmount.value = grainAmount;\n u.uGrainScale.value = grainScale;\n u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n u.uContrast.value = contrast;\n u.uGamma.value = gamma;\n u.uSaturation.value = saturation;\n u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n u.uZoom.value = zoom;\n u.uColor1.value = new Float32Array(hexToRgb(color1));\n u.uColor2.value = new Float32Array(hexToRgb(color2));\n u.uColor3.value = new Float32Array(hexToRgb(color3));\n }, [\n timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,\n warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,\n grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,\n centerX, centerY, zoom, color1, color2, color3\n ]);\n\n\n return ;\n};\n\nexport default Grainient;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Grainient-JS-TW.json b/public/r/Grainient-JS-TW.json
index 0c69ebee..f59f498a 100644
--- a/public/r/Grainient-JS-TW.json
+++ b/public/r/Grainient-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "Grainient/Grainient.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\nconst Grainient = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n\n const container = containerRef.current;\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: timeSpeed },\n uColorBalance: { value: colorBalance },\n uWarpStrength: { value: warpStrength },\n uWarpFrequency: { value: warpFrequency },\n uWarpSpeed: { value: warpSpeed },\n uWarpAmplitude: { value: warpAmplitude },\n uBlendAngle: { value: blendAngle },\n uBlendSoftness: { value: blendSoftness },\n uRotationAmount: { value: rotationAmount },\n uNoiseScale: { value: noiseScale },\n uGrainAmount: { value: grainAmount },\n uGrainScale: { value: grainScale },\n uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },\n uContrast: { value: contrast },\n uGamma: { value: gamma },\n uSaturation: { value: saturation },\n uCenterOffset: { value: new Float32Array([centerX, centerY]) },\n uZoom: { value: zoom },\n uColor1: { value: new Float32Array(hexToRgb(color1)) },\n uColor2: { value: new Float32Array(hexToRgb(color2)) },\n uColor3: { value: new Float32Array(hexToRgb(color3)) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width));\n const height = Math.max(1, Math.floor(rect.height));\n renderer.setSize(width, height);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n const t0 = performance.now();\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n try {\n container.removeChild(canvas);\n } catch {\n // Ignore\n }\n };\n }, [\n timeSpeed,\n colorBalance,\n warpStrength,\n warpFrequency,\n warpSpeed,\n warpAmplitude,\n blendAngle,\n blendSoftness,\n rotationAmount,\n noiseScale,\n grainAmount,\n grainScale,\n grainAnimated,\n contrast,\n gamma,\n saturation,\n centerX,\n centerY,\n zoom,\n color1,\n color2,\n color3\n ]);\n\n return ;\n};\n\nexport default Grainient;\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\n\nconst hexToRgb = hex => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\n\n// Keep renderer/program alive across re-renders so Effect 2 can update\n// uniforms without ever rebuilding the WebGL context.\nconst ctxMap = new WeakMap();\n\nconst Grainient = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n // Effect 1: build WebGL context once, pause when offscreen / tab hidden\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: 0.25 },\n uColorBalance: { value: 0.0 },\n uWarpStrength: { value: 1.0 },\n uWarpFrequency: { value: 5.0 },\n uWarpSpeed: { value: 2.0 },\n uWarpAmplitude: { value: 50.0 },\n uBlendAngle: { value: 0.0 },\n uBlendSoftness: { value: 0.05 },\n uRotationAmount: { value: 500.0 },\n uNoiseScale: { value: 2.0 },\n uGrainAmount: { value: 0.1 },\n uGrainScale: { value: 2.0 },\n uGrainAnimated: { value: 0.0 },\n uContrast: { value: 1.5 },\n uGamma: { value: 1.0 },\n uSaturation: { value: 1.0 },\n uCenterOffset: { value: new Float32Array([0, 0]) },\n uZoom: { value: 0.9 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = program.uniforms.iResolution.value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = t => {\n program.uniforms.iTime.value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n ctxMap.delete(container);\n try { container.removeChild(canvas); } catch { /* ignore */ }\n };\n }, []); // renderer created once\n\n // Effect 2: sync props to uniforms — zero GPU cost, no teardown\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms;\n\n u.uTimeSpeed.value = timeSpeed;\n u.uColorBalance.value = colorBalance;\n u.uWarpStrength.value = warpStrength;\n u.uWarpFrequency.value = warpFrequency;\n u.uWarpSpeed.value = warpSpeed;\n u.uWarpAmplitude.value = warpAmplitude;\n u.uBlendAngle.value = blendAngle;\n u.uBlendSoftness.value = blendSoftness;\n u.uRotationAmount.value = rotationAmount;\n u.uNoiseScale.value = noiseScale;\n u.uGrainAmount.value = grainAmount;\n u.uGrainScale.value = grainScale;\n u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n u.uContrast.value = contrast;\n u.uGamma.value = gamma;\n u.uSaturation.value = saturation;\n u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n u.uZoom.value = zoom;\n u.uColor1.value = new Float32Array(hexToRgb(color1));\n u.uColor2.value = new Float32Array(hexToRgb(color2));\n u.uColor3.value = new Float32Array(hexToRgb(color3));\n }, [\n timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,\n warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,\n grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,\n centerX, centerY, zoom, color1, color2, color3\n ]);\n\n\n return ;\n};\n\nexport default Grainient;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Grainient-TS-CSS.json b/public/r/Grainient-TS-CSS.json
index 5c1300ed..a58375bf 100644
--- a/public/r/Grainient-TS-CSS.json
+++ b/public/r/Grainient-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "Grainient/Grainient.tsx",
- "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Grainient.css';\n\ninterface GrainientProps {\n timeSpeed?: number;\n colorBalance?: number;\n warpStrength?: number;\n warpFrequency?: number;\n warpSpeed?: number;\n warpAmplitude?: number;\n blendAngle?: number;\n blendSoftness?: number;\n rotationAmount?: number;\n noiseScale?: number;\n grainAmount?: number;\n grainScale?: number;\n grainAnimated?: boolean;\n contrast?: number;\n gamma?: number;\n saturation?: number;\n centerX?: number;\n centerY?: number;\n zoom?: number;\n color1?: string;\n color2?: string;\n color3?: string;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\nconst Grainient: React.FC = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n\n const container = containerRef.current;\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: timeSpeed },\n uColorBalance: { value: colorBalance },\n uWarpStrength: { value: warpStrength },\n uWarpFrequency: { value: warpFrequency },\n uWarpSpeed: { value: warpSpeed },\n uWarpAmplitude: { value: warpAmplitude },\n uBlendAngle: { value: blendAngle },\n uBlendSoftness: { value: blendSoftness },\n uRotationAmount: { value: rotationAmount },\n uNoiseScale: { value: noiseScale },\n uGrainAmount: { value: grainAmount },\n uGrainScale: { value: grainScale },\n uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },\n uContrast: { value: contrast },\n uGamma: { value: gamma },\n uSaturation: { value: saturation },\n uCenterOffset: { value: new Float32Array([centerX, centerY]) },\n uZoom: { value: zoom },\n uColor1: { value: new Float32Array(hexToRgb(color1)) },\n uColor2: { value: new Float32Array(hexToRgb(color2)) },\n uColor3: { value: new Float32Array(hexToRgb(color3)) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width));\n const height = Math.max(1, Math.floor(rect.height));\n renderer.setSize(width, height);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n const t0 = performance.now();\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n try {\n container.removeChild(canvas);\n } catch {\n // Ignore\n }\n };\n }, [\n timeSpeed,\n colorBalance,\n warpStrength,\n warpFrequency,\n warpSpeed,\n warpAmplitude,\n blendAngle,\n blendSoftness,\n rotationAmount,\n noiseScale,\n grainAmount,\n grainScale,\n grainAnimated,\n contrast,\n gamma,\n saturation,\n centerX,\n centerY,\n zoom,\n color1,\n color2,\n color3\n ]);\n\n return ;\n};\n\nexport default Grainient;\n"
+ "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\nimport './Grainient.css';\n\ninterface GrainientProps {\n timeSpeed?: number;\n colorBalance?: number;\n warpStrength?: number;\n warpFrequency?: number;\n warpSpeed?: number;\n warpAmplitude?: number;\n blendAngle?: number;\n blendSoftness?: number;\n rotationAmount?: number;\n noiseScale?: number;\n grainAmount?: number;\n grainScale?: number;\n grainAnimated?: boolean;\n contrast?: number;\n gamma?: number;\n saturation?: number;\n centerX?: number;\n centerY?: number;\n zoom?: number;\n color1?: string;\n color2?: string;\n color3?: string;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\n\n// Keep renderer/program alive across re-renders so Effect 2 can update\n// uniforms without ever rebuilding the WebGL context.\ntype GrainientCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst Grainient: React.FC = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n // Effect 1: build WebGL context once, pause when offscreen / tab hidden\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: 0.25 },\n uColorBalance: { value: 0.0 },\n uWarpStrength: { value: 1.0 },\n uWarpFrequency: { value: 5.0 },\n uWarpSpeed: { value: 2.0 },\n uWarpAmplitude: { value: 50.0 },\n uBlendAngle: { value: 0.0 },\n uBlendSoftness: { value: 0.05 },\n uRotationAmount: { value: 500.0 },\n uNoiseScale: { value: 2.0 },\n uGrainAmount: { value: 0.1 },\n uGrainScale: { value: 2.0 },\n uGrainAnimated: { value: 0.0 },\n uContrast: { value: 1.5 },\n uGamma: { value: 1.0 },\n uSaturation: { value: 1.0 },\n uCenterOffset: { value: new Float32Array([0, 0]) },\n uZoom: { value: 0.9 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n ctxMap.delete(container);\n try { container.removeChild(canvas); } catch { /* ignore */ }\n };\n }, []); // renderer created once\n\n // Effect 2: sync props to uniforms — zero GPU cost, no teardown\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms as Record;\n\n u.uTimeSpeed.value = timeSpeed;\n u.uColorBalance.value = colorBalance;\n u.uWarpStrength.value = warpStrength;\n u.uWarpFrequency.value = warpFrequency;\n u.uWarpSpeed.value = warpSpeed;\n u.uWarpAmplitude.value = warpAmplitude;\n u.uBlendAngle.value = blendAngle;\n u.uBlendSoftness.value = blendSoftness;\n u.uRotationAmount.value = rotationAmount;\n u.uNoiseScale.value = noiseScale;\n u.uGrainAmount.value = grainAmount;\n u.uGrainScale.value = grainScale;\n u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n u.uContrast.value = contrast;\n u.uGamma.value = gamma;\n u.uSaturation.value = saturation;\n u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n u.uZoom.value = zoom;\n u.uColor1.value = new Float32Array(hexToRgb(color1));\n u.uColor2.value = new Float32Array(hexToRgb(color2));\n u.uColor3.value = new Float32Array(hexToRgb(color3));\n }, [\n timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,\n warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,\n grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,\n centerX, centerY, zoom, color1, color2, color3\n ]);\n\n\n return ;\n};\n\nexport default Grainient;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Grainient-TS-TW.json b/public/r/Grainient-TS-TW.json
index 8199a758..ce65df11 100644
--- a/public/r/Grainient-TS-TW.json
+++ b/public/r/Grainient-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "Grainient/Grainient.tsx",
- "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\n\ninterface GrainientProps {\n timeSpeed?: number;\n colorBalance?: number;\n warpStrength?: number;\n warpFrequency?: number;\n warpSpeed?: number;\n warpAmplitude?: number;\n blendAngle?: number;\n blendSoftness?: number;\n rotationAmount?: number;\n noiseScale?: number;\n grainAmount?: number;\n grainScale?: number;\n grainAnimated?: boolean;\n contrast?: number;\n gamma?: number;\n saturation?: number;\n centerX?: number;\n centerY?: number;\n zoom?: number;\n color1?: string;\n color2?: string;\n color3?: string;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\nconst Grainient: React.FC = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n\n const container = containerRef.current;\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: timeSpeed },\n uColorBalance: { value: colorBalance },\n uWarpStrength: { value: warpStrength },\n uWarpFrequency: { value: warpFrequency },\n uWarpSpeed: { value: warpSpeed },\n uWarpAmplitude: { value: warpAmplitude },\n uBlendAngle: { value: blendAngle },\n uBlendSoftness: { value: blendSoftness },\n uRotationAmount: { value: rotationAmount },\n uNoiseScale: { value: noiseScale },\n uGrainAmount: { value: grainAmount },\n uGrainScale: { value: grainScale },\n uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },\n uContrast: { value: contrast },\n uGamma: { value: gamma },\n uSaturation: { value: saturation },\n uCenterOffset: { value: new Float32Array([centerX, centerY]) },\n uZoom: { value: zoom },\n uColor1: { value: new Float32Array(hexToRgb(color1)) },\n uColor2: { value: new Float32Array(hexToRgb(color2)) },\n uColor3: { value: new Float32Array(hexToRgb(color3)) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width));\n const height = Math.max(1, Math.floor(rect.height));\n renderer.setSize(width, height);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n const t0 = performance.now();\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n try {\n container.removeChild(canvas);\n } catch {\n // Ignore\n }\n };\n }, [\n timeSpeed,\n colorBalance,\n warpStrength,\n warpFrequency,\n warpSpeed,\n warpAmplitude,\n blendAngle,\n blendSoftness,\n rotationAmount,\n noiseScale,\n grainAmount,\n grainScale,\n grainAnimated,\n contrast,\n gamma,\n saturation,\n centerX,\n centerY,\n zoom,\n color1,\n color2,\n color3\n ]);\n\n return ;\n};\n\nexport default Grainient;\n"
+ "content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle } from 'ogl';\n\ninterface GrainientProps {\n timeSpeed?: number;\n colorBalance?: number;\n warpStrength?: number;\n warpFrequency?: number;\n warpSpeed?: number;\n warpAmplitude?: number;\n blendAngle?: number;\n blendSoftness?: number;\n rotationAmount?: number;\n noiseScale?: number;\n grainAmount?: number;\n grainScale?: number;\n grainAnimated?: boolean;\n contrast?: number;\n gamma?: number;\n saturation?: number;\n centerX?: number;\n centerY?: number;\n zoom?: number;\n color1?: string;\n color2?: string;\n color3?: string;\n className?: string;\n}\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) return [1, 1, 1];\n return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n float t=iTime*uTimeSpeed;\n vec2 uv=C/iResolution.xy;\n float ratio=iResolution.x/iResolution.y;\n vec2 tuv=uv-0.5+uCenterOffset;\n tuv/=max(uZoom,0.001);\n\n float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n tuv.y*=1.0/ratio;\n tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n tuv.y*=ratio;\n\n float frequency=uWarpFrequency;\n float ws=max(uWarpStrength,0.001);\n float amplitude=uWarpAmplitude/ws;\n float warpTime=t*uWarpSpeed;\n tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n vec3 colLav=uColor1;\n vec3 colOrg=uColor2;\n vec3 colDark=uColor3;\n float b=uColorBalance;\n float s=max(uBlendSoftness,0.0);\n mat2 blendRot=Rot(radians(uBlendAngle));\n float blendX=(tuv*blendRot).x;\n float edge0=-0.3-b-s;\n float edge1=0.2-b+s;\n float v0=0.5-b+s;\n float v1=-0.3-b-s;\n vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n vec2 grainUv=uv*max(uGrainScale,0.001);\n if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n col+=(grain-0.5)*uGrainAmount;\n\n col=(col-0.5)*uContrast+0.5;\n float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n col=mix(vec3(luma),col,uSaturation);\n col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n col=clamp(col,0.0,1.0);\n\n o=vec4(col,1.0);\n}\nvoid main(){\n vec4 o=vec4(0.0);\n mainImage(o,gl_FragCoord.xy);\n fragColor=o;\n}\n`;\n\n\n// Keep renderer/program alive across re-renders so Effect 2 can update\n// uniforms without ever rebuilding the WebGL context.\ntype GrainientCtx = {\n renderer: InstanceType;\n program: InstanceType;\n mesh: InstanceType;\n};\nconst ctxMap = new WeakMap();\n\nconst Grainient: React.FC = ({\n timeSpeed = 0.25,\n colorBalance = 0.0,\n warpStrength = 1.0,\n warpFrequency = 5.0,\n warpSpeed = 2.0,\n warpAmplitude = 50.0,\n blendAngle = 0.0,\n blendSoftness = 0.05,\n rotationAmount = 500.0,\n noiseScale = 2.0,\n grainAmount = 0.1,\n grainScale = 2.0,\n grainAnimated = false,\n contrast = 1.5,\n gamma = 1.0,\n saturation = 1.0,\n centerX = 0.0,\n centerY = 0.0,\n zoom = 0.9,\n color1 = '#FF9FFC',\n color2 = '#5227FF',\n color3 = '#B19EEF',\n className = ''\n}) => {\n const containerRef = useRef(null);\n\n // Effect 1: build WebGL context once, pause when offscreen / tab hidden\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new Renderer({\n webgl: 2,\n alpha: true,\n antialias: false,\n dpr: Math.min(window.devicePixelRatio || 1, 2)\n });\n\n const gl = renderer.gl;\n const canvas = gl.canvas as HTMLCanvasElement;\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Float32Array([1, 1]) },\n uTimeSpeed: { value: 0.25 },\n uColorBalance: { value: 0.0 },\n uWarpStrength: { value: 1.0 },\n uWarpFrequency: { value: 5.0 },\n uWarpSpeed: { value: 2.0 },\n uWarpAmplitude: { value: 50.0 },\n uBlendAngle: { value: 0.0 },\n uBlendSoftness: { value: 0.05 },\n uRotationAmount: { value: 500.0 },\n uNoiseScale: { value: 2.0 },\n uGrainAmount: { value: 0.1 },\n uGrainScale: { value: 2.0 },\n uGrainAnimated: { value: 0.0 },\n uContrast: { value: 1.5 },\n uGamma: { value: 1.0 },\n uSaturation: { value: 1.0 },\n uCenterOffset: { value: new Float32Array([0, 0]) },\n uZoom: { value: 0.9 },\n uColor1: { value: new Float32Array([1, 1, 1]) },\n uColor2: { value: new Float32Array([1, 1, 1]) },\n uColor3: { value: new Float32Array([1, 1, 1]) }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n ctxMap.set(container, { renderer, program, mesh });\n\n const setSize = () => {\n const rect = container.getBoundingClientRect();\n const w = Math.max(1, Math.floor(rect.width));\n const h = Math.max(1, Math.floor(rect.height));\n renderer.setSize(w, h);\n const res = (program.uniforms.iResolution as { value: Float32Array }).value;\n res[0] = gl.drawingBufferWidth;\n res[1] = gl.drawingBufferHeight;\n };\n\n const ro = new ResizeObserver(setSize);\n ro.observe(container);\n setSize();\n\n let raf = 0;\n let isVisible = true;\n let isPageVisible = !document.hidden;\n const t0 = performance.now();\n\n const loop = (t: number) => {\n (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n renderer.render({ scene: mesh });\n raf = requestAnimationFrame(loop);\n };\n\n const tryStart = () => {\n if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);\n };\n const tryStop = () => {\n if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }\n };\n\n const io = new IntersectionObserver(\n ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },\n { threshold: 0 }\n );\n io.observe(container);\n\n const onVisibility = () => {\n isPageVisible = !document.hidden;\n isPageVisible ? tryStart() : tryStop();\n };\n document.addEventListener('visibilitychange', onVisibility);\n\n tryStart();\n\n return () => {\n tryStop();\n ro.disconnect();\n io.disconnect();\n document.removeEventListener('visibilitychange', onVisibility);\n ctxMap.delete(container);\n try { container.removeChild(canvas); } catch { /* ignore */ }\n };\n }, []); // renderer created once\n\n // Effect 2: sync props to uniforms — zero GPU cost, no teardown\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ctx = ctxMap.get(container);\n if (!ctx) return;\n const { program } = ctx;\n const u = program.uniforms as Record;\n\n u.uTimeSpeed.value = timeSpeed;\n u.uColorBalance.value = colorBalance;\n u.uWarpStrength.value = warpStrength;\n u.uWarpFrequency.value = warpFrequency;\n u.uWarpSpeed.value = warpSpeed;\n u.uWarpAmplitude.value = warpAmplitude;\n u.uBlendAngle.value = blendAngle;\n u.uBlendSoftness.value = blendSoftness;\n u.uRotationAmount.value = rotationAmount;\n u.uNoiseScale.value = noiseScale;\n u.uGrainAmount.value = grainAmount;\n u.uGrainScale.value = grainScale;\n u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n u.uContrast.value = contrast;\n u.uGamma.value = gamma;\n u.uSaturation.value = saturation;\n u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n u.uZoom.value = zoom;\n u.uColor1.value = new Float32Array(hexToRgb(color1));\n u.uColor2.value = new Float32Array(hexToRgb(color2));\n u.uColor3.value = new Float32Array(hexToRgb(color3));\n }, [\n timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,\n warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,\n grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,\n centerX, centerY, zoom, color1, color2, color3\n ]);\n\n\n return ;\n};\n\nexport default Grainient;\n"
}
],
"registryDependencies": [],
diff --git a/src/content/Backgrounds/Grainient/Grainient.jsx b/src/content/Backgrounds/Grainient/Grainient.jsx
index 8683ac07..0318f0fa 100644
--- a/src/content/Backgrounds/Grainient/Grainient.jsx
+++ b/src/content/Backgrounds/Grainient/Grainient.jsx
@@ -99,6 +99,11 @@ void main(){
}
`;
+
+// Keep renderer/program alive across re-renders so Effect 2 can update
+// uniforms without ever rebuilding the WebGL context.
+const ctxMap = new WeakMap();
+
const Grainient = ({
timeSpeed = 0.25,
colorBalance = 0.0,
@@ -126,8 +131,10 @@ const Grainient = ({
}) => {
const containerRef = useRef(null);
+ // Effect 1: build WebGL context once, pause when offscreen / tab hidden
useEffect(() => {
- if (!containerRef.current) return;
+ const container = containerRef.current;
+ if (!container) return;
const renderer = new Renderer({
webgl: 2,
@@ -141,8 +148,6 @@ const Grainient = ({
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
-
- const container = containerRef.current;
container.appendChild(canvas);
const geometry = new Triangle(gl);
@@ -150,39 +155,40 @@ const Grainient = ({
vertex,
fragment,
uniforms: {
- iTime: { value: 0 },
- iResolution: { value: new Float32Array([1, 1]) },
- uTimeSpeed: { value: timeSpeed },
- uColorBalance: { value: colorBalance },
- uWarpStrength: { value: warpStrength },
- uWarpFrequency: { value: warpFrequency },
- uWarpSpeed: { value: warpSpeed },
- uWarpAmplitude: { value: warpAmplitude },
- uBlendAngle: { value: blendAngle },
- uBlendSoftness: { value: blendSoftness },
- uRotationAmount: { value: rotationAmount },
- uNoiseScale: { value: noiseScale },
- uGrainAmount: { value: grainAmount },
- uGrainScale: { value: grainScale },
- uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },
- uContrast: { value: contrast },
- uGamma: { value: gamma },
- uSaturation: { value: saturation },
- uCenterOffset: { value: new Float32Array([centerX, centerY]) },
- uZoom: { value: zoom },
- uColor1: { value: new Float32Array(hexToRgb(color1)) },
- uColor2: { value: new Float32Array(hexToRgb(color2)) },
- uColor3: { value: new Float32Array(hexToRgb(color3)) }
+ iTime: { value: 0 },
+ iResolution: { value: new Float32Array([1, 1]) },
+ uTimeSpeed: { value: 0.25 },
+ uColorBalance: { value: 0.0 },
+ uWarpStrength: { value: 1.0 },
+ uWarpFrequency: { value: 5.0 },
+ uWarpSpeed: { value: 2.0 },
+ uWarpAmplitude: { value: 50.0 },
+ uBlendAngle: { value: 0.0 },
+ uBlendSoftness: { value: 0.05 },
+ uRotationAmount: { value: 500.0 },
+ uNoiseScale: { value: 2.0 },
+ uGrainAmount: { value: 0.1 },
+ uGrainScale: { value: 2.0 },
+ uGrainAnimated: { value: 0.0 },
+ uContrast: { value: 1.5 },
+ uGamma: { value: 1.0 },
+ uSaturation: { value: 1.0 },
+ uCenterOffset: { value: new Float32Array([0, 0]) },
+ uZoom: { value: 0.9 },
+ uColor1: { value: new Float32Array([1, 1, 1]) },
+ uColor2: { value: new Float32Array([1, 1, 1]) },
+ uColor3: { value: new Float32Array([1, 1, 1]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
+ ctxMap.set(container, { renderer, program, mesh });
const setSize = () => {
const rect = container.getBoundingClientRect();
- const width = Math.max(1, Math.floor(rect.width));
- const height = Math.max(1, Math.floor(rect.height));
- renderer.setSize(width, height);
+ const w = Math.max(1, Math.floor(rect.width));
+ const h = Math.max(1, Math.floor(rect.height));
+ renderer.setSize(w, h);
const res = program.uniforms.iResolution.value;
res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight;
@@ -193,48 +199,85 @@ const Grainient = ({
setSize();
let raf = 0;
+ let isVisible = true;
+ let isPageVisible = !document.hidden;
const t0 = performance.now();
+
const loop = t => {
program.uniforms.iTime.value = (t - t0) * 0.001;
renderer.render({ scene: mesh });
raf = requestAnimationFrame(loop);
};
- raf = requestAnimationFrame(loop);
+
+ const tryStart = () => {
+ if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);
+ };
+ const tryStop = () => {
+ if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }
+ };
+
+ const io = new IntersectionObserver(
+ ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },
+ { threshold: 0 }
+ );
+ io.observe(container);
+
+ const onVisibility = () => {
+ isPageVisible = !document.hidden;
+ isPageVisible ? tryStart() : tryStop();
+ };
+ document.addEventListener('visibilitychange', onVisibility);
+
+ tryStart();
return () => {
- cancelAnimationFrame(raf);
+ tryStop();
ro.disconnect();
- try {
- container.removeChild(canvas);
- } catch {
- // Ignore
- }
+ io.disconnect();
+ document.removeEventListener('visibilitychange', onVisibility);
+ ctxMap.delete(container);
+ try { container.removeChild(canvas); } catch { /* ignore */ }
};
+ }, []); // renderer created once
+
+ // Effect 2: sync props to uniforms — zero GPU cost, no teardown
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+ const ctx = ctxMap.get(container);
+ if (!ctx) return;
+ const { program } = ctx;
+ const u = program.uniforms;
+
+ u.uTimeSpeed.value = timeSpeed;
+ u.uColorBalance.value = colorBalance;
+ u.uWarpStrength.value = warpStrength;
+ u.uWarpFrequency.value = warpFrequency;
+ u.uWarpSpeed.value = warpSpeed;
+ u.uWarpAmplitude.value = warpAmplitude;
+ u.uBlendAngle.value = blendAngle;
+ u.uBlendSoftness.value = blendSoftness;
+ u.uRotationAmount.value = rotationAmount;
+ u.uNoiseScale.value = noiseScale;
+ u.uGrainAmount.value = grainAmount;
+ u.uGrainScale.value = grainScale;
+ u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;
+ u.uContrast.value = contrast;
+ u.uGamma.value = gamma;
+ u.uSaturation.value = saturation;
+ u.uCenterOffset.value = new Float32Array([centerX, centerY]);
+ u.uZoom.value = zoom;
+ u.uColor1.value = new Float32Array(hexToRgb(color1));
+ u.uColor2.value = new Float32Array(hexToRgb(color2));
+ u.uColor3.value = new Float32Array(hexToRgb(color3));
}, [
- timeSpeed,
- colorBalance,
- warpStrength,
- warpFrequency,
- warpSpeed,
- warpAmplitude,
- blendAngle,
- blendSoftness,
- rotationAmount,
- noiseScale,
- grainAmount,
- grainScale,
- grainAnimated,
- contrast,
- gamma,
- saturation,
- centerX,
- centerY,
- zoom,
- color1,
- color2,
- color3
+ timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,
+ warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,
+ grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,
+ centerX, centerY, zoom, color1, color2, color3
]);
+
return ;
};
diff --git a/src/tailwind/Backgrounds/Grainient/Grainient.jsx b/src/tailwind/Backgrounds/Grainient/Grainient.jsx
index 293d988b..a7ba966a 100644
--- a/src/tailwind/Backgrounds/Grainient/Grainient.jsx
+++ b/src/tailwind/Backgrounds/Grainient/Grainient.jsx
@@ -98,6 +98,11 @@ void main(){
}
`;
+
+// Keep renderer/program alive across re-renders so Effect 2 can update
+// uniforms without ever rebuilding the WebGL context.
+const ctxMap = new WeakMap();
+
const Grainient = ({
timeSpeed = 0.25,
colorBalance = 0.0,
@@ -125,8 +130,10 @@ const Grainient = ({
}) => {
const containerRef = useRef(null);
+ // Effect 1: build WebGL context once, pause when offscreen / tab hidden
useEffect(() => {
- if (!containerRef.current) return;
+ const container = containerRef.current;
+ if (!container) return;
const renderer = new Renderer({
webgl: 2,
@@ -140,8 +147,6 @@ const Grainient = ({
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
-
- const container = containerRef.current;
container.appendChild(canvas);
const geometry = new Triangle(gl);
@@ -149,39 +154,40 @@ const Grainient = ({
vertex,
fragment,
uniforms: {
- iTime: { value: 0 },
- iResolution: { value: new Float32Array([1, 1]) },
- uTimeSpeed: { value: timeSpeed },
- uColorBalance: { value: colorBalance },
- uWarpStrength: { value: warpStrength },
- uWarpFrequency: { value: warpFrequency },
- uWarpSpeed: { value: warpSpeed },
- uWarpAmplitude: { value: warpAmplitude },
- uBlendAngle: { value: blendAngle },
- uBlendSoftness: { value: blendSoftness },
- uRotationAmount: { value: rotationAmount },
- uNoiseScale: { value: noiseScale },
- uGrainAmount: { value: grainAmount },
- uGrainScale: { value: grainScale },
- uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },
- uContrast: { value: contrast },
- uGamma: { value: gamma },
- uSaturation: { value: saturation },
- uCenterOffset: { value: new Float32Array([centerX, centerY]) },
- uZoom: { value: zoom },
- uColor1: { value: new Float32Array(hexToRgb(color1)) },
- uColor2: { value: new Float32Array(hexToRgb(color2)) },
- uColor3: { value: new Float32Array(hexToRgb(color3)) }
+ iTime: { value: 0 },
+ iResolution: { value: new Float32Array([1, 1]) },
+ uTimeSpeed: { value: 0.25 },
+ uColorBalance: { value: 0.0 },
+ uWarpStrength: { value: 1.0 },
+ uWarpFrequency: { value: 5.0 },
+ uWarpSpeed: { value: 2.0 },
+ uWarpAmplitude: { value: 50.0 },
+ uBlendAngle: { value: 0.0 },
+ uBlendSoftness: { value: 0.05 },
+ uRotationAmount: { value: 500.0 },
+ uNoiseScale: { value: 2.0 },
+ uGrainAmount: { value: 0.1 },
+ uGrainScale: { value: 2.0 },
+ uGrainAnimated: { value: 0.0 },
+ uContrast: { value: 1.5 },
+ uGamma: { value: 1.0 },
+ uSaturation: { value: 1.0 },
+ uCenterOffset: { value: new Float32Array([0, 0]) },
+ uZoom: { value: 0.9 },
+ uColor1: { value: new Float32Array([1, 1, 1]) },
+ uColor2: { value: new Float32Array([1, 1, 1]) },
+ uColor3: { value: new Float32Array([1, 1, 1]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
+ ctxMap.set(container, { renderer, program, mesh });
const setSize = () => {
const rect = container.getBoundingClientRect();
- const width = Math.max(1, Math.floor(rect.width));
- const height = Math.max(1, Math.floor(rect.height));
- renderer.setSize(width, height);
+ const w = Math.max(1, Math.floor(rect.width));
+ const h = Math.max(1, Math.floor(rect.height));
+ renderer.setSize(w, h);
const res = program.uniforms.iResolution.value;
res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight;
@@ -192,48 +198,85 @@ const Grainient = ({
setSize();
let raf = 0;
+ let isVisible = true;
+ let isPageVisible = !document.hidden;
const t0 = performance.now();
+
const loop = t => {
program.uniforms.iTime.value = (t - t0) * 0.001;
renderer.render({ scene: mesh });
raf = requestAnimationFrame(loop);
};
- raf = requestAnimationFrame(loop);
+
+ const tryStart = () => {
+ if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);
+ };
+ const tryStop = () => {
+ if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }
+ };
+
+ const io = new IntersectionObserver(
+ ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },
+ { threshold: 0 }
+ );
+ io.observe(container);
+
+ const onVisibility = () => {
+ isPageVisible = !document.hidden;
+ isPageVisible ? tryStart() : tryStop();
+ };
+ document.addEventListener('visibilitychange', onVisibility);
+
+ tryStart();
return () => {
- cancelAnimationFrame(raf);
+ tryStop();
ro.disconnect();
- try {
- container.removeChild(canvas);
- } catch {
- // Ignore
- }
+ io.disconnect();
+ document.removeEventListener('visibilitychange', onVisibility);
+ ctxMap.delete(container);
+ try { container.removeChild(canvas); } catch { /* ignore */ }
};
+ }, []); // renderer created once
+
+ // Effect 2: sync props to uniforms — zero GPU cost, no teardown
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+ const ctx = ctxMap.get(container);
+ if (!ctx) return;
+ const { program } = ctx;
+ const u = program.uniforms;
+
+ u.uTimeSpeed.value = timeSpeed;
+ u.uColorBalance.value = colorBalance;
+ u.uWarpStrength.value = warpStrength;
+ u.uWarpFrequency.value = warpFrequency;
+ u.uWarpSpeed.value = warpSpeed;
+ u.uWarpAmplitude.value = warpAmplitude;
+ u.uBlendAngle.value = blendAngle;
+ u.uBlendSoftness.value = blendSoftness;
+ u.uRotationAmount.value = rotationAmount;
+ u.uNoiseScale.value = noiseScale;
+ u.uGrainAmount.value = grainAmount;
+ u.uGrainScale.value = grainScale;
+ u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;
+ u.uContrast.value = contrast;
+ u.uGamma.value = gamma;
+ u.uSaturation.value = saturation;
+ u.uCenterOffset.value = new Float32Array([centerX, centerY]);
+ u.uZoom.value = zoom;
+ u.uColor1.value = new Float32Array(hexToRgb(color1));
+ u.uColor2.value = new Float32Array(hexToRgb(color2));
+ u.uColor3.value = new Float32Array(hexToRgb(color3));
}, [
- timeSpeed,
- colorBalance,
- warpStrength,
- warpFrequency,
- warpSpeed,
- warpAmplitude,
- blendAngle,
- blendSoftness,
- rotationAmount,
- noiseScale,
- grainAmount,
- grainScale,
- grainAnimated,
- contrast,
- gamma,
- saturation,
- centerX,
- centerY,
- zoom,
- color1,
- color2,
- color3
+ timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,
+ warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,
+ grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,
+ centerX, centerY, zoom, color1, color2, color3
]);
+
return ;
};
diff --git a/src/ts-default/Backgrounds/Grainient/Grainient.tsx b/src/ts-default/Backgrounds/Grainient/Grainient.tsx
index 69d43d33..ae630cab 100644
--- a/src/ts-default/Backgrounds/Grainient/Grainient.tsx
+++ b/src/ts-default/Backgrounds/Grainient/Grainient.tsx
@@ -125,6 +125,16 @@ void main(){
}
`;
+
+// Keep renderer/program alive across re-renders so Effect 2 can update
+// uniforms without ever rebuilding the WebGL context.
+type GrainientCtx = {
+ renderer: InstanceType;
+ program: InstanceType;
+ mesh: InstanceType;
+};
+const ctxMap = new WeakMap();
+
const Grainient: React.FC = ({
timeSpeed = 0.25,
colorBalance = 0.0,
@@ -152,8 +162,10 @@ const Grainient: React.FC = ({
}) => {
const containerRef = useRef(null);
+ // Effect 1: build WebGL context once, pause when offscreen / tab hidden
useEffect(() => {
- if (!containerRef.current) return;
+ const container = containerRef.current;
+ if (!container) return;
const renderer = new Renderer({
webgl: 2,
@@ -167,8 +179,6 @@ const Grainient: React.FC = ({
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
-
- const container = containerRef.current;
container.appendChild(canvas);
const geometry = new Triangle(gl);
@@ -176,39 +186,40 @@ const Grainient: React.FC = ({
vertex,
fragment,
uniforms: {
- iTime: { value: 0 },
- iResolution: { value: new Float32Array([1, 1]) },
- uTimeSpeed: { value: timeSpeed },
- uColorBalance: { value: colorBalance },
- uWarpStrength: { value: warpStrength },
- uWarpFrequency: { value: warpFrequency },
- uWarpSpeed: { value: warpSpeed },
- uWarpAmplitude: { value: warpAmplitude },
- uBlendAngle: { value: blendAngle },
- uBlendSoftness: { value: blendSoftness },
- uRotationAmount: { value: rotationAmount },
- uNoiseScale: { value: noiseScale },
- uGrainAmount: { value: grainAmount },
- uGrainScale: { value: grainScale },
- uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },
- uContrast: { value: contrast },
- uGamma: { value: gamma },
- uSaturation: { value: saturation },
- uCenterOffset: { value: new Float32Array([centerX, centerY]) },
- uZoom: { value: zoom },
- uColor1: { value: new Float32Array(hexToRgb(color1)) },
- uColor2: { value: new Float32Array(hexToRgb(color2)) },
- uColor3: { value: new Float32Array(hexToRgb(color3)) }
+ iTime: { value: 0 },
+ iResolution: { value: new Float32Array([1, 1]) },
+ uTimeSpeed: { value: 0.25 },
+ uColorBalance: { value: 0.0 },
+ uWarpStrength: { value: 1.0 },
+ uWarpFrequency: { value: 5.0 },
+ uWarpSpeed: { value: 2.0 },
+ uWarpAmplitude: { value: 50.0 },
+ uBlendAngle: { value: 0.0 },
+ uBlendSoftness: { value: 0.05 },
+ uRotationAmount: { value: 500.0 },
+ uNoiseScale: { value: 2.0 },
+ uGrainAmount: { value: 0.1 },
+ uGrainScale: { value: 2.0 },
+ uGrainAnimated: { value: 0.0 },
+ uContrast: { value: 1.5 },
+ uGamma: { value: 1.0 },
+ uSaturation: { value: 1.0 },
+ uCenterOffset: { value: new Float32Array([0, 0]) },
+ uZoom: { value: 0.9 },
+ uColor1: { value: new Float32Array([1, 1, 1]) },
+ uColor2: { value: new Float32Array([1, 1, 1]) },
+ uColor3: { value: new Float32Array([1, 1, 1]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
+ ctxMap.set(container, { renderer, program, mesh });
const setSize = () => {
const rect = container.getBoundingClientRect();
- const width = Math.max(1, Math.floor(rect.width));
- const height = Math.max(1, Math.floor(rect.height));
- renderer.setSize(width, height);
+ const w = Math.max(1, Math.floor(rect.width));
+ const h = Math.max(1, Math.floor(rect.height));
+ renderer.setSize(w, h);
const res = (program.uniforms.iResolution as { value: Float32Array }).value;
res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight;
@@ -219,48 +230,85 @@ const Grainient: React.FC = ({
setSize();
let raf = 0;
+ let isVisible = true;
+ let isPageVisible = !document.hidden;
const t0 = performance.now();
+
const loop = (t: number) => {
(program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;
renderer.render({ scene: mesh });
raf = requestAnimationFrame(loop);
};
- raf = requestAnimationFrame(loop);
+
+ const tryStart = () => {
+ if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);
+ };
+ const tryStop = () => {
+ if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }
+ };
+
+ const io = new IntersectionObserver(
+ ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },
+ { threshold: 0 }
+ );
+ io.observe(container);
+
+ const onVisibility = () => {
+ isPageVisible = !document.hidden;
+ isPageVisible ? tryStart() : tryStop();
+ };
+ document.addEventListener('visibilitychange', onVisibility);
+
+ tryStart();
return () => {
- cancelAnimationFrame(raf);
+ tryStop();
ro.disconnect();
- try {
- container.removeChild(canvas);
- } catch {
- // Ignore
- }
+ io.disconnect();
+ document.removeEventListener('visibilitychange', onVisibility);
+ ctxMap.delete(container);
+ try { container.removeChild(canvas); } catch { /* ignore */ }
};
+ }, []); // renderer created once
+
+ // Effect 2: sync props to uniforms — zero GPU cost, no teardown
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+ const ctx = ctxMap.get(container);
+ if (!ctx) return;
+ const { program } = ctx;
+ const u = program.uniforms as Record;
+
+ u.uTimeSpeed.value = timeSpeed;
+ u.uColorBalance.value = colorBalance;
+ u.uWarpStrength.value = warpStrength;
+ u.uWarpFrequency.value = warpFrequency;
+ u.uWarpSpeed.value = warpSpeed;
+ u.uWarpAmplitude.value = warpAmplitude;
+ u.uBlendAngle.value = blendAngle;
+ u.uBlendSoftness.value = blendSoftness;
+ u.uRotationAmount.value = rotationAmount;
+ u.uNoiseScale.value = noiseScale;
+ u.uGrainAmount.value = grainAmount;
+ u.uGrainScale.value = grainScale;
+ u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;
+ u.uContrast.value = contrast;
+ u.uGamma.value = gamma;
+ u.uSaturation.value = saturation;
+ u.uCenterOffset.value = new Float32Array([centerX, centerY]);
+ u.uZoom.value = zoom;
+ u.uColor1.value = new Float32Array(hexToRgb(color1));
+ u.uColor2.value = new Float32Array(hexToRgb(color2));
+ u.uColor3.value = new Float32Array(hexToRgb(color3));
}, [
- timeSpeed,
- colorBalance,
- warpStrength,
- warpFrequency,
- warpSpeed,
- warpAmplitude,
- blendAngle,
- blendSoftness,
- rotationAmount,
- noiseScale,
- grainAmount,
- grainScale,
- grainAnimated,
- contrast,
- gamma,
- saturation,
- centerX,
- centerY,
- zoom,
- color1,
- color2,
- color3
+ timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,
+ warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,
+ grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,
+ centerX, centerY, zoom, color1, color2, color3
]);
+
return ;
};
diff --git a/src/ts-tailwind/Backgrounds/Grainient/Grainient.tsx b/src/ts-tailwind/Backgrounds/Grainient/Grainient.tsx
index bee21048..e6ed1f5f 100644
--- a/src/ts-tailwind/Backgrounds/Grainient/Grainient.tsx
+++ b/src/ts-tailwind/Backgrounds/Grainient/Grainient.tsx
@@ -124,6 +124,16 @@ void main(){
}
`;
+
+// Keep renderer/program alive across re-renders so Effect 2 can update
+// uniforms without ever rebuilding the WebGL context.
+type GrainientCtx = {
+ renderer: InstanceType;
+ program: InstanceType;
+ mesh: InstanceType;
+};
+const ctxMap = new WeakMap();
+
const Grainient: React.FC = ({
timeSpeed = 0.25,
colorBalance = 0.0,
@@ -151,8 +161,10 @@ const Grainient: React.FC = ({
}) => {
const containerRef = useRef(null);
+ // Effect 1: build WebGL context once, pause when offscreen / tab hidden
useEffect(() => {
- if (!containerRef.current) return;
+ const container = containerRef.current;
+ if (!container) return;
const renderer = new Renderer({
webgl: 2,
@@ -166,8 +178,6 @@ const Grainient: React.FC = ({
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
-
- const container = containerRef.current;
container.appendChild(canvas);
const geometry = new Triangle(gl);
@@ -175,39 +185,40 @@ const Grainient: React.FC = ({
vertex,
fragment,
uniforms: {
- iTime: { value: 0 },
- iResolution: { value: new Float32Array([1, 1]) },
- uTimeSpeed: { value: timeSpeed },
- uColorBalance: { value: colorBalance },
- uWarpStrength: { value: warpStrength },
- uWarpFrequency: { value: warpFrequency },
- uWarpSpeed: { value: warpSpeed },
- uWarpAmplitude: { value: warpAmplitude },
- uBlendAngle: { value: blendAngle },
- uBlendSoftness: { value: blendSoftness },
- uRotationAmount: { value: rotationAmount },
- uNoiseScale: { value: noiseScale },
- uGrainAmount: { value: grainAmount },
- uGrainScale: { value: grainScale },
- uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },
- uContrast: { value: contrast },
- uGamma: { value: gamma },
- uSaturation: { value: saturation },
- uCenterOffset: { value: new Float32Array([centerX, centerY]) },
- uZoom: { value: zoom },
- uColor1: { value: new Float32Array(hexToRgb(color1)) },
- uColor2: { value: new Float32Array(hexToRgb(color2)) },
- uColor3: { value: new Float32Array(hexToRgb(color3)) }
+ iTime: { value: 0 },
+ iResolution: { value: new Float32Array([1, 1]) },
+ uTimeSpeed: { value: 0.25 },
+ uColorBalance: { value: 0.0 },
+ uWarpStrength: { value: 1.0 },
+ uWarpFrequency: { value: 5.0 },
+ uWarpSpeed: { value: 2.0 },
+ uWarpAmplitude: { value: 50.0 },
+ uBlendAngle: { value: 0.0 },
+ uBlendSoftness: { value: 0.05 },
+ uRotationAmount: { value: 500.0 },
+ uNoiseScale: { value: 2.0 },
+ uGrainAmount: { value: 0.1 },
+ uGrainScale: { value: 2.0 },
+ uGrainAnimated: { value: 0.0 },
+ uContrast: { value: 1.5 },
+ uGamma: { value: 1.0 },
+ uSaturation: { value: 1.0 },
+ uCenterOffset: { value: new Float32Array([0, 0]) },
+ uZoom: { value: 0.9 },
+ uColor1: { value: new Float32Array([1, 1, 1]) },
+ uColor2: { value: new Float32Array([1, 1, 1]) },
+ uColor3: { value: new Float32Array([1, 1, 1]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
+ ctxMap.set(container, { renderer, program, mesh });
const setSize = () => {
const rect = container.getBoundingClientRect();
- const width = Math.max(1, Math.floor(rect.width));
- const height = Math.max(1, Math.floor(rect.height));
- renderer.setSize(width, height);
+ const w = Math.max(1, Math.floor(rect.width));
+ const h = Math.max(1, Math.floor(rect.height));
+ renderer.setSize(w, h);
const res = (program.uniforms.iResolution as { value: Float32Array }).value;
res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight;
@@ -218,48 +229,85 @@ const Grainient: React.FC = ({
setSize();
let raf = 0;
+ let isVisible = true;
+ let isPageVisible = !document.hidden;
const t0 = performance.now();
+
const loop = (t: number) => {
(program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;
renderer.render({ scene: mesh });
raf = requestAnimationFrame(loop);
};
- raf = requestAnimationFrame(loop);
+
+ const tryStart = () => {
+ if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);
+ };
+ const tryStop = () => {
+ if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; }
+ };
+
+ const io = new IntersectionObserver(
+ ([entry]) => { isVisible = entry.isIntersecting; isVisible ? tryStart() : tryStop(); },
+ { threshold: 0 }
+ );
+ io.observe(container);
+
+ const onVisibility = () => {
+ isPageVisible = !document.hidden;
+ isPageVisible ? tryStart() : tryStop();
+ };
+ document.addEventListener('visibilitychange', onVisibility);
+
+ tryStart();
return () => {
- cancelAnimationFrame(raf);
+ tryStop();
ro.disconnect();
- try {
- container.removeChild(canvas);
- } catch {
- // Ignore
- }
+ io.disconnect();
+ document.removeEventListener('visibilitychange', onVisibility);
+ ctxMap.delete(container);
+ try { container.removeChild(canvas); } catch { /* ignore */ }
};
+ }, []); // renderer created once
+
+ // Effect 2: sync props to uniforms — zero GPU cost, no teardown
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+ const ctx = ctxMap.get(container);
+ if (!ctx) return;
+ const { program } = ctx;
+ const u = program.uniforms as Record;
+
+ u.uTimeSpeed.value = timeSpeed;
+ u.uColorBalance.value = colorBalance;
+ u.uWarpStrength.value = warpStrength;
+ u.uWarpFrequency.value = warpFrequency;
+ u.uWarpSpeed.value = warpSpeed;
+ u.uWarpAmplitude.value = warpAmplitude;
+ u.uBlendAngle.value = blendAngle;
+ u.uBlendSoftness.value = blendSoftness;
+ u.uRotationAmount.value = rotationAmount;
+ u.uNoiseScale.value = noiseScale;
+ u.uGrainAmount.value = grainAmount;
+ u.uGrainScale.value = grainScale;
+ u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;
+ u.uContrast.value = contrast;
+ u.uGamma.value = gamma;
+ u.uSaturation.value = saturation;
+ u.uCenterOffset.value = new Float32Array([centerX, centerY]);
+ u.uZoom.value = zoom;
+ u.uColor1.value = new Float32Array(hexToRgb(color1));
+ u.uColor2.value = new Float32Array(hexToRgb(color2));
+ u.uColor3.value = new Float32Array(hexToRgb(color3));
}, [
- timeSpeed,
- colorBalance,
- warpStrength,
- warpFrequency,
- warpSpeed,
- warpAmplitude,
- blendAngle,
- blendSoftness,
- rotationAmount,
- noiseScale,
- grainAmount,
- grainScale,
- grainAnimated,
- contrast,
- gamma,
- saturation,
- centerX,
- centerY,
- zoom,
- color1,
- color2,
- color3
+ timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,
+ warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale,
+ grainAmount, grainScale, grainAnimated, contrast, gamma, saturation,
+ centerX, centerY, zoom, color1, color2, color3
]);
+
return ;
};