diff --git a/frontend/public/assets/ichannel1.png b/frontend/public/assets/ichannel1.png new file mode 100644 index 0000000..a02f0a0 Binary files /dev/null and b/frontend/public/assets/ichannel1.png differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/ShaderBackground.tsx b/frontend/src/ShaderBackground.tsx new file mode 100644 index 0000000..f089020 --- /dev/null +++ b/frontend/src/ShaderBackground.tsx @@ -0,0 +1,205 @@ +import { useEffect, useRef } from "react"; + +import SHADER_CODE from "./assets/shader.glsl?raw"; + +function buildProgram( + ctx: WebGL2RenderingContext, + fragShader: WebGLShader, + vertexShader: WebGLShader +) { + const program = ctx.createProgram(); + if (!program) return null; + + ctx.attachShader(program, fragShader); + ctx.attachShader(program, vertexShader); + + ctx.linkProgram(program); + + const status = ctx.getProgramParameter(program, ctx.LINK_STATUS); + + if (status) { + return program; + } + + console.error(ctx.getProgramInfoLog(program)); + ctx.deleteProgram(program); + return null; +} + +function buildShader( + ctx: WebGL2RenderingContext, + type: number, + source: string +) { + const shader = ctx.createShader(type); + if (!shader) return null; + + ctx.shaderSource(shader, source); + ctx.compileShader(shader); + + const status = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS); + + if (status) { + return shader; + } + + console.error(ctx.getShaderInfoLog(shader)); + ctx.deleteShader(shader); + return null; +} + +function loadTexture(gl: WebGL2RenderingContext, url: string) { + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Because images have to be downloaded over the internet + // they might take a moment until they are ready. + // Until then put a single pixel in the texture so we can + // use it immediately. When the image has finished downloading + // we'll update the texture with the contents of the image. + const level = 0; + const internalFormat = gl.RGBA; + const width = 1; + const height = 1; + const border = 0; + const srcFormat = gl.RGBA; + const srcType = gl.UNSIGNED_BYTE; + const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue + gl.texImage2D( + gl.TEXTURE_2D, + level, + internalFormat, + width, + height, + border, + srcFormat, + srcType, + pixel + ); + + const image = new Image(); + image.onload = () => { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + level, + internalFormat, + srcFormat, + srcType, + image + ); + + // WebGL1 has different requirements for power of 2 images + // vs. non power of 2 images so check if the image is a + // power of 2 in both dimensions. + if (isPowerOf2(image.width) && isPowerOf2(image.height)) { + // Yes, it's a power of 2. Generate mips. + gl.generateMipmap(gl.TEXTURE_2D); + } else { + // No, it's not a power of 2. Turn off mips and set + // wrapping to clamp to edge + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + } + }; + image.src = url; + + return texture; +} + +function isPowerOf2(value: number) { + return (value & (value - 1)) === 0; +} + + +export const ShaderBackground = () => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const gl = canvas.getContext("webgl2"); + if (!gl) return; + + gl.viewport(0, 0, canvas.width, canvas.height); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + const shader = buildShader(gl, gl.FRAGMENT_SHADER, SHADER_CODE); + const vertexShader = buildShader( + gl, + gl.VERTEX_SHADER, + `#version 300 es + precision lowp float; + + in vec2 a_position; + + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + }` + ); + + if (!shader || !vertexShader) return; + + const finalProgram = buildProgram(gl, shader, vertexShader); + + if (!finalProgram) return; + + const posBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), + gl.STATIC_DRAW + ); + + gl.useProgram(finalProgram); + const loc = gl.getAttribLocation(finalProgram, "a_position"); + gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(loc); + + function setResolution(program: WebGLProgram) { + const loc = gl!.getUniformLocation(program, "iResolution"); + if (loc) gl!.uniform3fv(loc, [canvas!.width, canvas!.height, 1]); + } + + function setTime(program: WebGLProgram, now: number) { + const loc = gl!.getUniformLocation(program, "iTime"); + if (loc) gl!.uniform1f(loc, now); + } + + const final_iChannel1 = gl.getUniformLocation(finalProgram, "iChannel1"); + + const ichannel1_texture = loadTexture(gl, "assets/ichannel1.png"); + + gl!.viewport(0, 0, canvas!.width, canvas!.height); + + gl!.useProgram(finalProgram); + setResolution(finalProgram!); + + // Bind Texture to Unit 1 + gl!.activeTexture(gl!.TEXTURE1); + gl!.bindTexture(gl!.TEXTURE_2D, ichannel1_texture); + gl!.uniform1i(final_iChannel1, 1); + + function update(now: number) { + // time in seconds + const time = (now) / 1000; + gl!.clear(gl!.COLOR_BUFFER_BIT); + + setTime(finalProgram!, time); + + gl!.drawArrays(gl!.TRIANGLES, 0, 6); + + requestAnimationFrame(update); + } + + requestAnimationFrame(update); + }); + + return ( + + ); +}; diff --git a/frontend/src/ShaderBackground_regret.tsx b/frontend/src/ShaderBackground_regret.tsx deleted file mode 100644 index 5bcfca6..0000000 --- a/frontend/src/ShaderBackground_regret.tsx +++ /dev/null @@ -1,1165 +0,0 @@ -import { useEffect, useRef } from "react"; -import { generateRandomRGBABytes } from "./randomTextureForShader"; - -const buffer_1 = `#version 300 es -precision mediump float; -uniform vec3 iResolution; // viewport resolution (in pixels) -uniform float iTime; // shader playback time (in seconds) -uniform float iTimeDelta; // render time (in seconds) -uniform float iFrameRate; // shader frame rate -uniform int iFrame; // shader playback frame -uniform float iChannelTime[4]; // channel playback time (in seconds) -uniform vec3 iChannelResolution[4]; // channel resolution (in pixels) -uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click -uniform sampler2D iChannel0; -uniform sampler2D iChannel1; -uniform sampler2D iChannel2; -uniform sampler2D iChannel3; - -out vec4 FragColor; - -/* -Main render. - -Temporal AA for a smooth image. Temporal accumulation is disabled while moving the view to prevent ghosting. -*/ - -#define ITERATIONS 200 //Increase for less grainy result -#define TEMPORAL_AA - - -const vec3 MainColor = vec3(1.0); - -//noise code by iq -float noise( in vec3 x ) -{ - vec3 p = floor(x); - vec3 f = fract(x); - f = f*f*(3.0-2.0*f); - vec2 uv = (p.xy+vec2(37.0,17.0)*p.z) + f.xy; - vec2 rg = textureLod( iChannel0, (uv+ 0.5)/256.0, 0.0 ).yx; - return -1.0+2.0*mix( rg.x, rg.y, f.z ); -} - -float saturate(float x) -{ - return clamp(x, 0.0, 1.0); -} - -vec3 saturate(vec3 x) -{ - return clamp(x, vec3(0.0), vec3(1.0)); -} - -float rand(vec2 coord) -{ - return saturate(fract(sin(dot(coord, vec2(12.9898, 78.223))) * 43758.5453)); -} - -float pcurve( float x, float a, float b ) -{ - float k = pow(a+b,a+b) / (pow(a,a)*pow(b,b)); - return k * pow( x, a ) * pow( 1.0-x, b ); -} - -const float pi = 3.14159265; - -float atan2(float y, float x) -{ - if (x > 0.0) - { - return atan(y / x); - } - else if (x == 0.0) - { - if (y > 0.0) - { - return pi / 2.0; - } - else if (y < 0.0) - { - return -(pi / 2.0); - } - else - { - return 0.0; - } - } - else //(x < 0.0) - { - if (y >= 0.0) - { - return atan(y / x) + pi; - } - else - { - return atan(y / x) - pi; - } - } -} - -float sdTorus(vec3 p, vec2 t) -{ - vec2 q = vec2(length(p.xz) - t.x, p.y); - return length(q)-t.y; -} - -float sdSphere(vec3 p, float r) -{ - return length(p)-r; -} - -void Haze(inout vec3 color, vec3 pos, float alpha) -{ - vec2 t = vec2(1.0, 0.01); - - float torusDist = length(sdTorus(pos + vec3(0.0, -0.05, 0.0), t)); - - float bloomDisc = 1.0 / (pow(torusDist, 2.0) + 0.001); - vec3 col = MainColor; - bloomDisc *= length(pos) < 0.5 ? 0.0 : 1.0; - - color += col * bloomDisc * (2.9 / float(ITERATIONS)) * (1.0 - alpha * 1.0); -} - -void GasDisc(inout vec3 color, inout float alpha, vec3 pos) -{ - float discRadius = 3.2; - float discWidth = 5.3; - float discInner = discRadius - discWidth * 0.5; - float discOuter = discRadius + discWidth * 0.5; - - vec3 origin = vec3(0.0, 0.0, 0.0); - float mouseZ = iMouse.y / iResolution.y; - vec3 discNormal = normalize(vec3(0.0, 1.0, 0.0)); - float discThickness = 0.1; - - float distFromCenter = distance(pos, origin); - float distFromDisc = dot(discNormal, pos - origin); - - float radialGradient = 1.0 - saturate((distFromCenter - discInner) / discWidth * 0.5); - - float coverage = pcurve(radialGradient, 4.0, 0.9); - - discThickness *= radialGradient; - coverage *= saturate(1.0 - abs(distFromDisc) / discThickness); - - vec3 dustColorLit = MainColor; - vec3 dustColorDark = vec3(0.0, 0.0, 0.0); - - float dustGlow = 1.0 / (pow(1.0 - radialGradient, 2.0) * 290.0 + 0.002); - vec3 dustColor = dustColorLit * dustGlow * 8.2; - - coverage = saturate(coverage * 0.7); - - - float fade = pow((abs(distFromCenter - discInner) + 0.4), 4.0) * 0.04; - float bloomFactor = 1.0 / (pow(distFromDisc, 2.0) * 40.0 + fade + 0.00002); - vec3 b = dustColorLit * pow(bloomFactor, 1.5); - - b *= mix(vec3(1.7, 1.1, 1.0), vec3(0.5, 0.6, 1.0), vec3(pow(radialGradient, 2.0))); - b *= mix(vec3(1.7, 0.5, 0.1), vec3(1.0), vec3(pow(radialGradient, 0.5))); - - dustColor = mix(dustColor, b * 150.0, saturate(1.0 - coverage * 1.0)); - coverage = saturate(coverage + bloomFactor * bloomFactor * 0.1); - - if (coverage < 0.01) - { - return; - } - - - vec3 radialCoords; - radialCoords.x = distFromCenter * 1.5 + 0.55; - radialCoords.y = atan2(-pos.x, -pos.z) * 1.5; - radialCoords.z = distFromDisc * 1.5; - - radialCoords *= 0.95; - - float speed = 0.06; - - float noise1 = 1.0; - vec3 rc = radialCoords + 0.0; rc.y += iTime * speed; - noise1 *= noise(rc * 3.0) * 0.5 + 0.5; rc.y -= iTime * speed; - noise1 *= noise(rc * 6.0) * 0.5 + 0.5; rc.y += iTime * speed; - noise1 *= noise(rc * 12.0) * 0.5 + 0.5; rc.y -= iTime * speed; - noise1 *= noise(rc * 24.0) * 0.5 + 0.5; rc.y += iTime * speed; - - float noise2 = 2.0; - rc = radialCoords + 30.0; - noise2 *= noise(rc * 3.0) * 0.5 + 0.5; rc.y += iTime * speed; - noise2 *= noise(rc * 6.0) * 0.5 + 0.5; rc.y -= iTime * speed; - noise2 *= noise(rc * 12.0) * 0.5 + 0.5; rc.y += iTime * speed; - noise2 *= noise(rc * 24.0) * 0.5 + 0.5; rc.y -= iTime * speed; - noise2 *= noise(rc * 48.0) * 0.5 + 0.5; rc.y += iTime * speed; - noise2 *= noise(rc * 92.0) * 0.5 + 0.5; rc.y -= iTime * speed; - - dustColor *= noise1 * 0.998 + 0.002; - coverage *= noise2; - - radialCoords.y += iTime * speed * 0.5; - - dustColor *= pow(texture(iChannel1, radialCoords.yx * vec2(0.15, 0.27)).rgb, vec3(2.0)) * 4.0; - - coverage = saturate(coverage * 1200.0 / float(ITERATIONS)); - dustColor = max(vec3(0.0), dustColor); - - coverage *= pcurve(radialGradient, 4.0, 0.9); - - color = (1.0 - alpha) * dustColor * coverage + color; - - alpha = (1.0 - alpha) * coverage + alpha; -} - - - -vec3 rotate(vec3 p, float x, float y, float z) -{ - mat3 matx = mat3(1.0, 0.0, 0.0, - 0.0, cos(x), sin(x), - 0.0, -sin(x), cos(x)); - - mat3 maty = mat3(cos(y), 0.0, -sin(y), - 0.0, 1.0, 0.0, - sin(y), 0.0, cos(y)); - - mat3 matz = mat3(cos(z), sin(z), 0.0, - -sin(z), cos(z), 0.0, - 0.0, 0.0, 1.0); - - p = matx * p; - p = matz * p; - p = maty * p; - - return p; -} - -void RotateCamera(inout vec3 eyevec, inout vec3 eyepos) -{ - float mousePosY = iMouse.y / iResolution.y; - float mousePosX = iMouse.x / iResolution.x; - - vec3 angle = vec3(mousePosY * 0.05 + 0.05, 1.0 + mousePosX * 1.0, -0.45); - - eyevec = rotate(eyevec, angle.x, angle.y, angle.z); - eyepos = rotate(eyepos, angle.x, angle.y, angle.z); -} - -void WarpSpace(inout vec3 eyevec, inout vec3 raypos) -{ - vec3 origin = vec3(0.0, 0.0, 0.0); - - float singularityDist = distance(raypos, origin); - float warpFactor = 1.0 / (pow(singularityDist, 2.0) + 0.000001); - - vec3 singularityVector = normalize(origin - raypos); - - float warpAmount = 5.0; - - eyevec = normalize(eyevec + singularityVector * warpFactor * warpAmount / float(ITERATIONS)); -} - -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - - vec2 uv = fragCoord.xy / iResolution.xy; - - float aspect = iResolution.x / iResolution.y; - - vec2 uveye = uv; - - #ifdef TEMPORAL_AA - uveye.x += (rand(uv + sin(iTime * 1.0)) / iResolution.x) * (iMouse.z > 1.0 ? 0.0 : 1.0); - uveye.y += (rand(uv + 1.0 + sin(iTime * 1.0)) / iResolution.y) * (iMouse.z > 1.0 ? 0.0 : 1.0); - #endif - - vec3 eyevec = normalize(vec3((uveye * 2.0 - 1.0) * vec2(aspect, 1.0), 6.0)); - vec3 eyepos = vec3(0.0, -0.0, -10.0); - - vec2 mousepos = iMouse.xy / iResolution.xy; - if (mousepos.x == 0.0) - { - mousepos.x = 0.35; - } - eyepos.x += mousepos.x * 3.0 - 1.5; - - const float far = 15.0; - - RotateCamera(eyevec, eyepos); - - vec3 color = vec3(0.0, 0.0, 0.0); - - float dither = rand(uv - #ifdef TEMPORAL_AA - + sin(iTime * 1.0) * (iMouse.z > 1.0 ? 0.0 : 1.0) - #endif - ) * 2.0; - - - float alpha = 0.0; - vec3 raypos = eyepos + eyevec * dither * far / float(ITERATIONS); - for (int i = 0; i < ITERATIONS; i++) - { - WarpSpace(eyevec, raypos); - raypos += eyevec * far / float(ITERATIONS); - GasDisc(color, alpha, raypos); - Haze(color, raypos, alpha); - } - - color *= 0.0001; - - - #ifdef TEMPORAL_AA - const float p = 1.0; - vec3 previous = pow(texture(iChannel2, uv).rgb, vec3(1.0 / p)); - - color = pow(color, vec3(1.0 / p)); - - float blendWeight = 0.9 * (iMouse.z > 1.0 ? 0.0 : 1.0); - - color = mix(color, previous, blendWeight); - - color = pow(color, vec3(p)); - #endif - - fragColor = vec4(saturate(color), 1.0); -} - -void main() -{ - // gl_FragCoord.xy is pixel coordinates (1..width, 1..height) - mainImage(FragColor, gl_FragCoord.xy); -} -`; - -const buffer_2 = `#version 300 es -precision mediump float; -uniform vec3 iResolution; // viewport resolution (in pixels) -uniform float iTime; // shader playback time (in seconds) -uniform float iTimeDelta; // render time (in seconds) -uniform float iFrameRate; // shader frame rate -uniform int iFrame; // shader playback frame -uniform float iChannelTime[4]; // channel playback time (in seconds) -uniform vec3 iChannelResolution[4]; // channel resolution (in pixels) -uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click -uniform sampler2D iChannel0; -uniform sampler2D iChannel1; -uniform sampler2D iChannel2; -uniform sampler2D iChannel3; - -out vec4 FragColor; -//First bloom pass, mipmap tree thing - -vec3 ColorFetch(vec2 coord) -{ - return texture(iChannel0, coord).rgb; -} - -vec3 Grab1(vec2 coord, const float octave, const vec2 offset) -{ - float scale = exp2(octave); - - coord += offset; - coord *= scale; - - if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) - { - return vec3(0.0); - } - - vec3 color = ColorFetch(coord); - - return color; -} - -vec3 Grab4(vec2 coord, const float octave, const vec2 offset) -{ - float scale = exp2(octave); - - coord += offset; - coord *= scale; - - if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) - { - return vec3(0.0); - } - - vec3 color = vec3(0.0); - float weights = 0.0; - - const int oversampling = 4; - - for (int i = 0; i < oversampling; i++) - { - for (int j = 0; j < oversampling; j++) - { - vec2 off = (vec2(i, j) / iResolution.xy + vec2(0.0) / iResolution.xy) * scale / float(oversampling); - color += ColorFetch(coord + off); - - - weights += 1.0; - } - } - - color /= weights; - - return color; -} - -vec3 Grab8(vec2 coord, const float octave, const vec2 offset) -{ - float scale = exp2(octave); - - coord += offset; - coord *= scale; - - if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) - { - return vec3(0.0); - } - - vec3 color = vec3(0.0); - float weights = 0.0; - - const int oversampling = 8; - - for (int i = 0; i < oversampling; i++) - { - for (int j = 0; j < oversampling; j++) - { - vec2 off = (vec2(i, j) / iResolution.xy + vec2(0.0) / iResolution.xy) * scale / float(oversampling); - color += ColorFetch(coord + off); - - - weights += 1.0; - } - } - - color /= weights; - - return color; -} - -vec3 Grab16(vec2 coord, const float octave, const vec2 offset) -{ - float scale = exp2(octave); - - coord += offset; - coord *= scale; - - if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) - { - return vec3(0.0); - } - - vec3 color = vec3(0.0); - float weights = 0.0; - - const int oversampling = 16; - - for (int i = 0; i < oversampling; i++) - { - for (int j = 0; j < oversampling; j++) - { - vec2 off = (vec2(i, j) / iResolution.xy + vec2(0.0) / iResolution.xy) * scale / float(oversampling); - color += ColorFetch(coord + off); - - - weights += 1.0; - } - } - - color /= weights; - - return color; -} - -vec2 CalcOffset(float octave) -{ - vec2 offset = vec2(0.0); - - vec2 padding = vec2(10.0) / iResolution.xy; - - offset.x = -min(1.0, floor(octave / 3.0)) * (0.25 + padding.x); - - offset.y = -(1.0 - (1.0 / exp2(octave))) - padding.y * octave; - - offset.y += min(1.0, floor(octave / 3.0)) * 0.35; - - return offset; -} - -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - vec2 uv = fragCoord.xy / iResolution.xy; - - - vec3 color = vec3(0.0); - - /* - Create a mipmap tree thingy with padding to prevent leaking bloom - - Since there's no mipmaps for the previous buffer and the reduction process has to be done in one pass, - oversampling is required for a proper result - */ - color += Grab1(uv, 1.0, vec2(0.0, 0.0) ); - color += Grab4(uv, 2.0, vec2(CalcOffset(1.0)) ); - color += Grab8(uv, 3.0, vec2(CalcOffset(2.0)) ); - color += Grab16(uv, 4.0, vec2(CalcOffset(3.0)) ); - color += Grab16(uv, 5.0, vec2(CalcOffset(4.0)) ); - color += Grab16(uv, 6.0, vec2(CalcOffset(5.0)) ); - color += Grab16(uv, 7.0, vec2(CalcOffset(6.0)) ); - color += Grab16(uv, 8.0, vec2(CalcOffset(7.0)) ); - - - fragColor = vec4(color, 1.0); -} - -void main() -{ - // gl_FragCoord.xy is pixel coordinates (1..width, 1..height) - mainImage(FragColor, gl_FragCoord.xy); -} -`; - -const buffer_3 = `#version 300 es -precision mediump float; -uniform vec3 iResolution; // viewport resolution (in pixels) -uniform float iTime; // shader playback time (in seconds) -uniform float iTimeDelta; // render time (in seconds) -uniform float iFrameRate; // shader frame rate -uniform int iFrame; // shader playback frame -uniform float iChannelTime[4]; // channel playback time (in seconds) -uniform vec3 iChannelResolution[4]; // channel resolution (in pixels) -uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click -uniform sampler2D iChannel0; -uniform sampler2D iChannel1; -uniform sampler2D iChannel2; -uniform sampler2D iChannel3; - -out vec4 FragColor; -//Horizontal gaussian blur leveraging hardware filtering for fewer texture lookups. - -vec3 ColorFetch(vec2 coord) -{ - return texture(iChannel0, coord).rgb; -} - -float weights[5]; -float offsets[5]; - - -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - - weights[0] = 0.19638062; - weights[1] = 0.29675293; - weights[2] = 0.09442139; - weights[3] = 0.01037598; - weights[4] = 0.00025940; - - offsets[0] = 0.00000000; - offsets[1] = 1.41176471; - offsets[2] = 3.29411765; - offsets[3] = 5.17647059; - offsets[4] = 7.05882353; - - vec2 uv = fragCoord.xy / iResolution.xy; - - vec3 color = vec3(0.0); - float weightSum = 0.0; - - if (uv.x < 0.52) - { - color += ColorFetch(uv) * weights[0]; - weightSum += weights[0]; - - for(int i = 1; i < 5; i++) - { - vec2 offset = vec2(offsets[i]) / iResolution.xy; - color += ColorFetch(uv + offset * vec2(0.5, 0.0)) * weights[i]; - color += ColorFetch(uv - offset * vec2(0.5, 0.0)) * weights[i]; - weightSum += weights[i] * 2.0; - } - - color /= weightSum; - } - - fragColor = vec4(color,1.0); -} - -void main() -{ - // gl_FragCoord.xy is pixel coordinates (1..width, 1..height) - mainImage(FragColor, gl_FragCoord.xy); -} -`; - -const buffer_4 = `#version 300 es -precision mediump float; -uniform vec3 iResolution; // viewport resolution (in pixels) -uniform float iTime; // shader playback time (in seconds) -uniform float iTimeDelta; // render time (in seconds) -uniform float iFrameRate; // shader frame rate -uniform int iFrame; // shader playback frame -uniform float iChannelTime[4]; // channel playback time (in seconds) -uniform vec3 iChannelResolution[4]; // channel resolution (in pixels) -uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click -uniform sampler2D iChannel0; -uniform sampler2D iChannel1; -uniform sampler2D iChannel2; -uniform sampler2D iChannel3; - -out vec4 FragColor; - -//Vertical gaussian blur leveraging hardware filtering for fewer texture lookups. - -vec3 ColorFetch(vec2 coord) -{ - return texture(iChannel0, coord).rgb; -} - -float weights[5]; -float offsets[5]; - - -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - - weights[0] = 0.19638062; - weights[1] = 0.29675293; - weights[2] = 0.09442139; - weights[3] = 0.01037598; - weights[4] = 0.00025940; - - offsets[0] = 0.00000000; - offsets[1] = 1.41176471; - offsets[2] = 3.29411765; - offsets[3] = 5.17647059; - offsets[4] = 7.05882353; - - vec2 uv = fragCoord.xy / iResolution.xy; - - vec3 color = vec3(0.0); - float weightSum = 0.0; - - if (uv.x < 0.52) - { - color += ColorFetch(uv) * weights[0]; - weightSum += weights[0]; - - for(int i = 1; i < 5; i++) - { - vec2 offset = vec2(offsets[i]) / iResolution.xy; - color += ColorFetch(uv + offset * vec2(0.0, 0.5)) * weights[i]; - color += ColorFetch(uv - offset * vec2(0.0, 0.5)) * weights[i]; - weightSum += weights[i] * 2.0; - } - - color /= weightSum; - } - - fragColor = vec4(color,1.0); -} - -void main() -{ - // gl_FragCoord.xy is pixel coordinates (1..width, 1..height) - mainImage(FragColor, gl_FragCoord.xy); -} -`; - -const image_1 = `#version 300 es -precision mediump float; -uniform vec3 iResolution; // viewport resolution (in pixels) -uniform float iTime; // shader playback time (in seconds) -uniform float iTimeDelta; // render time (in seconds) -uniform float iFrameRate; // shader frame rate -uniform int iFrame; // shader playback frame -uniform float iChannelTime[4]; // channel playback time (in seconds) -uniform vec3 iChannelResolution[4]; // channel resolution (in pixels) -uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click -uniform sampler2D iChannel0; -uniform sampler2D iChannel1; -uniform sampler2D iChannel2; -uniform sampler2D iChannel3; - -out vec4 FragColor; -vec3 saturate(vec3 x) -{ - return clamp(x, vec3(0.0), vec3(1.0)); -} - -vec4 cubic(float x) -{ - float x2 = x * x; - float x3 = x2 * x; - vec4 w; - w.x = -x3 + 3.0*x2 - 3.0*x + 1.0; - w.y = 3.0*x3 - 6.0*x2 + 4.0; - w.z = -3.0*x3 + 3.0*x2 + 3.0*x + 1.0; - w.w = x3; - return w / 6.0; -} - -vec4 BicubicTexture(in sampler2D tex, in vec2 coord) -{ - vec2 resolution = iResolution.xy; - - coord *= resolution; - - float fx = fract(coord.x); - float fy = fract(coord.y); - coord.x -= fx; - coord.y -= fy; - - fx -= 0.5; - fy -= 0.5; - - vec4 xcubic = cubic(fx); - vec4 ycubic = cubic(fy); - - vec4 c = vec4(coord.x - 0.5, coord.x + 1.5, coord.y - 0.5, coord.y + 1.5); - vec4 s = vec4(xcubic.x + xcubic.y, xcubic.z + xcubic.w, ycubic.x + ycubic.y, ycubic.z + ycubic.w); - vec4 offset = c + vec4(xcubic.y, xcubic.w, ycubic.y, ycubic.w) / s; - - vec4 sample0 = texture(tex, vec2(offset.x, offset.z) / resolution); - vec4 sample1 = texture(tex, vec2(offset.y, offset.z) / resolution); - vec4 sample2 = texture(tex, vec2(offset.x, offset.w) / resolution); - vec4 sample3 = texture(tex, vec2(offset.y, offset.w) / resolution); - - float sx = s.x / (s.x + s.y); - float sy = s.z / (s.z + s.w); - - return mix( mix(sample3, sample2, sx), mix(sample1, sample0, sx), sy); -} - -vec3 ColorFetch(vec2 coord) -{ - return texture(iChannel0, coord).rgb; -} - -vec3 BloomFetch(vec2 coord) -{ - return BicubicTexture(iChannel3, coord).rgb; -} - -vec3 Grab(vec2 coord, const float octave, const vec2 offset) -{ - float scale = exp2(octave); - - coord /= scale; - coord -= offset; - - return BloomFetch(coord); -} - -vec2 CalcOffset(float octave) -{ - vec2 offset = vec2(0.0); - - vec2 padding = vec2(10.0) / iResolution.xy; - - offset.x = -min(1.0, floor(octave / 3.0)) * (0.25 + padding.x); - - offset.y = -(1.0 - (1.0 / exp2(octave))) - padding.y * octave; - - offset.y += min(1.0, floor(octave / 3.0)) * 0.35; - - return offset; -} - -vec3 GetBloom(vec2 coord) -{ - vec3 bloom = vec3(0.0); - - //Reconstruct bloom from multiple blurred images - bloom += Grab(coord, 1.0, vec2(CalcOffset(0.0))) * 1.0; - bloom += Grab(coord, 2.0, vec2(CalcOffset(1.0))) * 1.5; - bloom += Grab(coord, 3.0, vec2(CalcOffset(2.0))) * 1.0; - bloom += Grab(coord, 4.0, vec2(CalcOffset(3.0))) * 1.5; - bloom += Grab(coord, 5.0, vec2(CalcOffset(4.0))) * 1.8; - bloom += Grab(coord, 6.0, vec2(CalcOffset(5.0))) * 1.0; - bloom += Grab(coord, 7.0, vec2(CalcOffset(6.0))) * 1.0; - bloom += Grab(coord, 8.0, vec2(CalcOffset(7.0))) * 1.0; - - return bloom; -} - -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - - vec2 uv = fragCoord.xy / iResolution.xy; - - vec3 color = ColorFetch(uv); - - - color += GetBloom(uv) * 0.08; - - color *= 200.0; - - - //Tonemapping and color grading - color = pow(color, vec3(1.5)); - color = color / (1.0 + color); - color = pow(color, vec3(1.0 / 1.5)); - - - color = mix(color, color * color * (3.0 - 2.0 * color), vec3(1.0)); - color = pow(color, vec3(1.3, 1.20, 1.0)); - - color = saturate(color * 1.01); - - color = pow(color, vec3(0.7 / 2.2)); - - fragColor = vec4(color, 1.0); - -} - -void main() -{ - // gl_FragCoord.xy is pixel coordinates (1..width, 1..height) - mainImage(FragColor, gl_FragCoord.xy); -} -`; - -function buildProgram( - ctx: WebGL2RenderingContext, - fragShader: WebGLShader, - vertexShader: WebGLShader -) { - const program = ctx.createProgram(); - if (!program) return null; - - ctx.attachShader(program, fragShader); - ctx.attachShader(program, vertexShader); - - ctx.linkProgram(program); - - const status = ctx.getProgramParameter(program, ctx.LINK_STATUS); - - if (status) { - return program; - } - - console.error(ctx.getProgramInfoLog(program)); - ctx.deleteProgram(program); - return null; -} - -function buildShader( - ctx: WebGL2RenderingContext, - type: number, - source: string -) { - const shader = ctx.createShader(type); - if (!shader) return null; - - ctx.shaderSource(shader, source); - ctx.compileShader(shader); - - const status = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS); - - if (status) { - return shader; - } - - console.error(ctx.getShaderInfoLog(shader)); - ctx.deleteShader(shader); - return null; -} - -export function ShaderBackground() { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const gl = canvas.getContext("webgl2"); - if (!gl) return; - - gl.viewport(0, 0, canvas.width, canvas.height); - gl.clearColor(0, 0, 0, 1); - gl.clear(gl.COLOR_BUFFER_BIT); - - const buffer_1_shader = buildShader(gl, gl.FRAGMENT_SHADER, buffer_1); - const buffer_2_shader = buildShader(gl, gl.FRAGMENT_SHADER, buffer_2); - const buffer_3_shader = buildShader(gl, gl.FRAGMENT_SHADER, buffer_3); - const buffer_4_shader = buildShader(gl, gl.FRAGMENT_SHADER, buffer_4); - const fragmentShader = buildShader(gl, gl.FRAGMENT_SHADER, image_1); - - const vertexShader = buildShader( - gl, - gl.VERTEX_SHADER, - `#version 300 es - precision mediump float; - - in vec2 a_position; - - void main() { - gl_Position = vec4(a_position, 0.0, 1.0); - }` - ); - - if ( - !buffer_1_shader || - !buffer_2_shader || - !buffer_3_shader || - !buffer_4_shader || - !fragmentShader || - !vertexShader - ) - return; - - const buffer_1_program = buildProgram(gl, buffer_1_shader, vertexShader); - const buffer_2_program = buildProgram(gl, buffer_2_shader, vertexShader); - const buffer_3_program = buildProgram(gl, buffer_3_shader, vertexShader); - const buffer_4_program = buildProgram(gl, buffer_4_shader, vertexShader); - const finalProgram = buildProgram(gl, fragmentShader, vertexShader); - - if ( - !buffer_1_program || - !buffer_2_program || - !buffer_3_program || - !buffer_4_program || - !finalProgram - ) - return; - - const posBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), - gl.STATIC_DRAW - ); - - // Vertex attributes for each program - const programs = [ - buffer_1_program, - buffer_2_program, - buffer_3_program, - buffer_4_program, - finalProgram, - ]; - programs.forEach((prog) => { - gl.useProgram(prog); - const loc = gl.getAttribLocation(prog, "a_position"); - gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(loc); - }); - - // --- HELPER FUNC --- - function createFramebufferTexture(w: number, h: number) { - const tex = gl!.createTexture(); - gl!.bindTexture(gl!.TEXTURE_2D, tex); - gl!.texImage2D( - gl!.TEXTURE_2D, - 0, - gl!.RGBA, - w, - h, - 0, - gl!.RGBA, - gl!.UNSIGNED_BYTE, - null - ); - gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MIN_FILTER, gl!.LINEAR); - gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MAG_FILTER, gl!.LINEAR); - gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_WRAP_S, gl!.CLAMP_TO_EDGE); - gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_WRAP_T, gl!.CLAMP_TO_EDGE); - - const fb = gl!.createFramebuffer(); - gl!.bindFramebuffer(gl!.DRAW_FRAMEBUFFER, fb); - gl!.framebufferTexture2D( - gl!.DRAW_FRAMEBUFFER, - gl!.COLOR_ATTACHMENT0, - gl!.TEXTURE_2D, - tex, - 0 - ); - - return { tex, fb }; - } - - // 1. Scene Buffer (Stores the main render) - const sceneTarget = createFramebufferTexture(canvas.width, canvas.height); - - // 2. Ping-Pong Buffers (For doing multipass bloom effects) - const pingTarget = createFramebufferTexture(canvas.width, canvas.height); - const pongTarget = createFramebufferTexture(canvas.width, canvas.height); - - // --- NOISE TEXTURE --- - // Fix: Create once, not every frame - const noiseBytes = generateRandomRGBABytes(canvas.width, canvas.height); - const noiseTexture = gl.createTexture(); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, noiseTexture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - canvas.width, - canvas.height, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - noiseBytes - ); - - function setResolution(program: WebGLProgram) { - const loc = gl!.getUniformLocation(program, "iResolution"); - if (loc) gl!.uniform3fv(loc, [canvas!.width, canvas!.height, 1]); - } - - function setTime(program: WebGLProgram, now: number) { - const loc = gl!.getUniformLocation(program, "iTime"); - if (loc) gl!.uniform1f(loc, now); - } - - // Cache uniform locations - const locs = { - b1_iChannel0: gl.getUniformLocation(buffer_1_program, "iChannel0"), - b2_iChannel0: gl.getUniformLocation(buffer_2_program, "iChannel0"), - b3_iChannel0: gl.getUniformLocation(buffer_3_program, "iChannel0"), - b4_iChannel0: gl.getUniformLocation(buffer_4_program, "iChannel0"), - final_iChannel0: gl.getUniformLocation(finalProgram, "iChannel0"), - final_iChannel3: gl.getUniformLocation(finalProgram, "iChannel3"), // Bloom result - }; - - let reqId: number; - const started = Date.now(); - - function update(now: number) { - // time in seconds - const time = (now - started) / 1000; - - // ---------------------------------------------------- - // PASS 1: Render SCENE -> sceneTarget - // ---------------------------------------------------- - gl!.bindFramebuffer(gl!.DRAW_FRAMEBUFFER, sceneTarget.fb); - gl!.viewport(0, 0, canvas!.width, canvas!.height); - gl!.clear(gl!.COLOR_BUFFER_BIT); - - gl!.useProgram(buffer_1_program); - setResolution(buffer_1_program!); - setTime(buffer_1_program!, time); - - // Bind Noise to Unit 0 - gl!.activeTexture(gl!.TEXTURE0); - gl!.bindTexture(gl!.TEXTURE_2D, noiseTexture); - gl!.uniform1i(locs.b1_iChannel0, 0); - - gl!.drawArrays(gl!.TRIANGLES, 0, 6); - - // ---------------------------------------------------- - // PASS 2: Bloom Prep (Buffer 2) -> pingTarget - // input: sceneTarget (Unit 0) - // ---------------------------------------------------- - gl!.bindFramebuffer(gl!.DRAW_FRAMEBUFFER, pingTarget.fb); - - gl!.useProgram(buffer_2_program); - setResolution(buffer_2_program!); - - gl!.activeTexture(gl!.TEXTURE0); - gl!.bindTexture(gl!.TEXTURE_2D, sceneTarget.tex); - gl!.uniform1i(locs.b2_iChannel0, 0); - - gl!.drawArrays(gl!.TRIANGLES, 0, 6); - - // ---------------------------------------------------- - // PASS 3: Horizontal Blur (Buffer 3) -> pongTarget - // input: pingTarget (Unit 0) - // ---------------------------------------------------- - gl!.bindFramebuffer(gl!.DRAW_FRAMEBUFFER, pongTarget.fb); - - gl!.useProgram(buffer_3_program); - setResolution(buffer_3_program!); - - gl!.activeTexture(gl!.TEXTURE0); - gl!.bindTexture(gl!.TEXTURE_2D, pingTarget.tex); - gl!.uniform1i(locs.b3_iChannel0, 0); - - gl!.drawArrays(gl!.TRIANGLES, 0, 6); - - // ---------------------------------------------------- - // PASS 4: Vertical Blur (Buffer 4) -> pingTarget (Final Bloom) - // input: pongTarget (Unit 0) - // ---------------------------------------------------- - // Reuse pingTarget to store the final bloom result - gl!.bindFramebuffer(gl!.DRAW_FRAMEBUFFER, pingTarget.fb); - - gl!.useProgram(buffer_4_program); - setResolution(buffer_4_program!); - - gl!.activeTexture(gl!.TEXTURE0); - gl!.bindTexture(gl!.TEXTURE_2D, pongTarget.tex); - gl!.uniform1i(locs.b4_iChannel0, 0); - - gl!.drawArrays(gl!.TRIANGLES, 0, 6); - - // ---------------------------------------------------- - // PASS 5: Final Composition -> Screen - // Inputs: - // - iChannel0: sceneTarget (Unit 0) - // - iChannel3: pingTarget aka Bloom Result (Unit 3) - // ---------------------------------------------------- - gl!.bindFramebuffer(gl!.DRAW_FRAMEBUFFER, null); // Screen - - gl!.useProgram(finalProgram); - setResolution(finalProgram!); - - // Bind Scene to Unit 0 - gl!.activeTexture(gl!.TEXTURE0); - gl!.bindTexture(gl!.TEXTURE_2D, sceneTarget.tex); - gl!.uniform1i(locs.final_iChannel0, 0); - - // Bind Bloom to Unit 3 - gl!.activeTexture(gl!.TEXTURE3); - gl!.bindTexture(gl!.TEXTURE_2D, pingTarget.tex); - gl!.uniform1i(locs.final_iChannel3, 3); // Tell shader iChannel3 is on unit 3 - - gl!.drawArrays(gl!.TRIANGLES, 0, 6); - - reqId = requestAnimationFrame(update); - } - - reqId = requestAnimationFrame(update); - - // CLEANUP - return () => { - cancelAnimationFrame(reqId); - gl!.deleteProgram(buffer_1_program); - gl!.deleteProgram(buffer_2_program); - gl!.deleteProgram(buffer_3_program); - gl!.deleteProgram(buffer_4_program); - gl!.deleteProgram(finalProgram); - - gl!.deleteShader(buffer_1_shader); - gl!.deleteShader(buffer_2_shader); - gl!.deleteShader(buffer_3_shader); - gl!.deleteShader(buffer_4_shader); - gl!.deleteShader(fragmentShader); - gl!.deleteShader(vertexShader); - - gl!.deleteBuffer(posBuffer); - - gl!.deleteTexture(noiseTexture); - gl!.deleteTexture(sceneTarget.tex); - gl!.deleteFramebuffer(sceneTarget.fb); - gl!.deleteTexture(pingTarget.tex); - gl!.deleteFramebuffer(pingTarget.fb); - gl!.deleteTexture(pongTarget.tex); - gl!.deleteFramebuffer(pongTarget.fb); - }; - }, []); - - return ( - - ); -} diff --git a/frontend/src/assets/shader.glsl b/frontend/src/assets/shader.glsl new file mode 100644 index 0000000..cb66698 --- /dev/null +++ b/frontend/src/assets/shader.glsl @@ -0,0 +1,83 @@ +#version 300 es +precision lowp float; +uniform vec3 iResolution; // viewport resolution (in pixels) +uniform float iTime; // shader playback time (in seconds) +uniform sampler2D iChannel1; + +out vec4 FragColor; + +const float pi = 3.1415927f; + +float sdSphere(vec3 p, float s) { + return length(p) - s; +} + +float sdTorus(vec3 p, vec2 t) { + vec2 q = vec2(length(p.xz) - t.x, p.y); + return length(q) - t.y; +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 pp = fragCoord.xy / iResolution.xy; + pp = -1.0f + 2.0f * pp; + pp.x *= iResolution.x / iResolution.y; + + vec3 lookAt = vec3(0.0f, -0.1f, 0.0f); + + float eyer = 2.0f; + float eyea = 0.0f; + float eyea2 = (-0.24f) * pi * 2.0f; + + vec3 ro = vec3(eyer * cos(eyea) * sin(eyea2), eyer * cos(eyea2), eyer * sin(eyea) * sin(eyea2)); //camera position + + vec3 front = normalize(lookAt - ro); + vec3 left = normalize(cross(normalize(vec3(0.0f, 1, -0.1f)), front)); + vec3 up = normalize(cross(front, left)); + vec3 rd = normalize(front * 1.5f + left * pp.x + up * pp.y); // rect vector + + vec3 bh = vec3(0.0f, 0.0f, 0.0f); + float bhr = 0.1f; + float bhmass = 5.0f; + bhmass *= 0.001f; // premul G + + vec3 p = ro; + vec3 pv = rd; + float dt = 0.02f; + + vec3 col = vec3(0.0f); + + float noncaptured = 1.0f; + + vec3 c1 = vec3(0.5f, 0.46f, 0.4f); + vec3 c2 = vec3(1.0f, 0.8f, 0.6f); + + for(float t = 0.0f; t < 1.0f; t += 0.005f) { + p += pv * dt * noncaptured; + + // gravity + vec3 bhv = bh - p; + float r = dot(bhv, bhv); + pv += normalize(bhv) * ((bhmass) / r); + + noncaptured = smoothstep(0.0f, 0.666f, sdSphere(p - bh, bhr)); + + // Texture for the accretion disc + float dr = length(bhv.xz); + float da = atan(bhv.x, bhv.z); + vec2 ra = vec2(dr, da * (0.01f + (dr - bhr) * 0.002f) + 2.0f * pi + iTime * 0.005f); + ra *= vec2(10.0f, 20.0f); + + vec3 dcol = mix(c2, c1, pow(length(bhv) - bhr, 2.0f)) * max(0.0f, texture(iChannel1, ra * vec2(0.1f, 0.5f)).r + 0.05f) * (4.0f / ((0.001f + (length(bhv) - bhr) * 50.0f))); + + col += max(vec3(0.0f), dcol * smoothstep(0.0f, 1.0f, -sdTorus((p * vec3(1.0f, 25.0f, 1.0f)) - bh, vec2(0.8f, 0.99f))) * noncaptured); + + col += vec3(1.0f, 0.9f, 0.85f) * (1.0f / vec3(dot(bhv, bhv))) * 0.0033f * noncaptured; + + } + fragColor = vec4(col, 1.0f); +} + +void main() { + // gl_FragCoord.xy is pixel coordinates (1..width, 1..height) + mainImage(FragColor, gl_FragCoord.xy); +} \ No newline at end of file diff --git a/frontend/src/randomTextureForShader.ts b/frontend/src/randomTextureForShader.ts deleted file mode 100644 index 57cc139..0000000 --- a/frontend/src/randomTextureForShader.ts +++ /dev/null @@ -1,35 +0,0 @@ -export function generateRandomRGBABytes( - width: number, - height: number -): Uint8Array { - if (width <= 0 || height <= 0) { - throw new Error("width and height must be positive"); - } - const len = width * height * 4; - const out = new Uint8Array(len); - - // Maximum bytes per getRandomValues call (per spec / browsers) - const MAX_GETRANDOM_BYTES = 65536; - - if (typeof crypto !== "undefined" && "getRandomValues" in crypto) { - // Fill in chunks of up to MAX_GETRANDOM_BYTES - let offset = 0; - while (offset < len) { - const chunkSize = Math.min(MAX_GETRANDOM_BYTES, len - offset); - // Subarray view for the current chunk - const chunkView = out.subarray(offset, offset + chunkSize); - crypto.getRandomValues(chunkView); - offset += chunkSize; - } - // Ensure alpha channel is fully opaque (255) - for (let i = 3; i < len; i += 4) out[i] = 255; - } else { - // Fallback to Math.random for all bytes - for (let i = 0; i < len; i++) { - if ((i + 1) % 4 === 0) out[i] = 255; - else out[i] = Math.floor(Math.random() * 256); - } - } - - return out; -}