From 4e91349a59055206948048ffb273586f5b556f8d Mon Sep 17 00:00:00 2001 From: code002lover Date: Sat, 6 Dec 2025 13:49:52 +0100 Subject: [PATCH] WIP Try adding black hole background --- frontend/src/App.tsx | 18 + frontend/src/ShaderBackground.tsx | 1028 ++++++++++++++++++++++++ frontend/src/index.css | 14 + frontend/src/randomTextureForShader.ts | 93 +++ 4 files changed, 1153 insertions(+) create mode 100644 frontend/src/ShaderBackground.tsx create mode 100644 frontend/src/randomTextureForShader.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index de7e48b..b279a8e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { PersonDetails } from "./PersonDetails"; import { GameList } from "./GameList"; import { GameFilter } from "./GameFilter"; import { GameDetails } from "./GameDetails"; +import { ShaderBackground } from "./ShaderBackground"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import "./App.css"; import { apiFetch } from "./api"; @@ -15,6 +16,15 @@ function App() { const [token, setToken] = useState( localStorage.getItem("token") || "" ); + const [isShaderTheme, setIsShaderTheme] = useState(false); + + useEffect(() => { + if (isShaderTheme) { + document.body.classList.add("shader-theme"); + } else { + document.body.classList.remove("shader-theme"); + } + }, [isShaderTheme]); const fetchPeople = () => { if (!token) return; @@ -66,7 +76,15 @@ function App() { + + {isShaderTheme && } } /> } /> diff --git a/frontend/src/ShaderBackground.tsx b/frontend/src/ShaderBackground.tsx new file mode 100644 index 0000000..782c68f --- /dev/null +++ b/frontend/src/ShaderBackground.tsx @@ -0,0 +1,1028 @@ +import { useEffect, useRef } from "react"; +import { createAndBindRandomTextureToIChannel0 } 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; + + function setResolution( + program: WebGLProgram, + canvas: HTMLCanvasElement, + gl: WebGL2RenderingContext + ) { + const loc = gl.getUniformLocation(program, "iResolution"); + if (loc === null) { + console.warn( + "iResolution uniform not found (maybe not used by shader)." + ); + } else { + // Optionally convert to physical pixels: + const dpr = window.devicePixelRatio || 1; + const w = canvas.clientWidth * dpr; + const h = canvas.clientHeight * dpr; + + // set as vec3: x = width, y = height, z = pixel aspect (often 1.0) + gl.uniform3f(loc, w, h, 1.0); + } + } + + function setTime( + program: WebGLProgram, + now: number, + gl: WebGL2RenderingContext + ) { + const loc = gl.getUniformLocation(program, "iTime"); + if (loc === null) { + console.warn( + "iTime uniform not found (maybe not used by shader).", + program + ); + } else { + gl.uniform1f(loc, now); + } + } + + function render( + gl: WebGL2RenderingContext, + canvas: HTMLCanvasElement, + now: number + ) { + //Clear with Clear color + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.useProgram(buffer_1_program); + setResolution(buffer_1_program as WebGLProgram, canvas, gl); + setTime(buffer_1_program as WebGLProgram, now, gl); + const tex = createAndBindRandomTextureToIChannel0( + gl, + buffer_1_program as WebGLProgram, + canvas.width, + canvas.height + ); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, tex); + + // RENDER 1 + gl.drawBuffers([gl.BACK]); + + gl.useProgram(buffer_2_program); + setResolution(buffer_2_program as WebGLProgram, canvas, gl); + + // RENDER 2 + gl.drawBuffers([gl.BACK]); + + gl.useProgram(buffer_3_program); + setResolution(buffer_3_program as WebGLProgram, canvas, gl); + + // RENDER 3 + gl.drawBuffers([gl.BACK]); + + gl.useProgram(buffer_4_program); + setResolution(buffer_4_program as WebGLProgram, canvas, gl); + + // RENDER 4 + gl.drawBuffers([gl.BACK]); + + gl.useProgram(finalProgram); + setResolution(finalProgram as WebGLProgram, canvas, gl); + + // FINAL RENDER + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + const started = Date.now(); + + function update(now: number) { + const time = (now - started) / 1000; + render(gl as WebGL2RenderingContext, canvas as HTMLCanvasElement, time); + + requestAnimationFrame(update); + } + + requestAnimationFrame(update); + }, []); + + return ; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index c425777..cde6255 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -88,7 +88,21 @@ input:focus, select:focus { border-color: transparent; } + ul { list-style: none; padding: 0; } + +.shader-theme { + --primary-bg: transparent; /* Let the shader show through */ + --secondary-bg: rgba(0, 0, 0, 0.7); /* Translucent cards */ + --secondary-alt-bg: rgba(20, 20, 20, 0.6); /* Translucent inputs */ + --tertiary-bg: rgba(40, 40, 40, 0.8); + --border-color: rgba(255, 255, 255, 0.15); + --text-color: #ffffff; +} + +.shader-theme body { + background-color: transparent; +} diff --git a/frontend/src/randomTextureForShader.ts b/frontend/src/randomTextureForShader.ts new file mode 100644 index 0000000..60d133a --- /dev/null +++ b/frontend/src/randomTextureForShader.ts @@ -0,0 +1,93 @@ +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; +} + +/** + * Create a WebGL texture from random data and bind it to texture unit 0, + * then set the sampler2D uniform named `iChannel0` to use unit 0. + * + * gl: WebGLRenderingContext or WebGL2RenderingContext + * program: compiled & linked shader program (must be in use or will be used) + */ +export function createAndBindRandomTextureToIChannel0( + gl: WebGLRenderingContext | WebGL2RenderingContext, + program: WebGLProgram, + width: number, + height: number +): WebGLTexture { + const bytes = generateRandomRGBABytes(width, height); + + const tex = gl.createTexture(); + if (!tex) throw new Error("Failed to create texture"); + + // Activate texture unit 0 and bind the texture + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, tex); + + // Texture parameters suitable for data textures + 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); + + // Upload pixel data as RGBA UNSIGNED_BYTE + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + width, + height, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + bytes + ); + + const location = gl.getUniformLocation(program, "iChannel0"); + if (location === null) { + // Clean up binding and return but signal with thrown error if desired + gl.bindTexture(gl.TEXTURE_2D, null); + throw new Error("Uniform 'iChannel0' not found in program"); + } + + // Set sampler uniform to texture unit 0 + gl.uniform1i(location, 0); + + // Unbind the texture if you prefer (not strictly necessary) + gl.bindTexture(gl.TEXTURE_2D, null); + + return tex; +}