diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fa7d1f4..feb681e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -84,6 +84,8 @@ function App() {
+
+
{isShaderTheme && }
diff --git a/frontend/src/ShaderBackground.tsx b/frontend/src/ShaderBackground.tsx
index 522806a..ece2667 100644
--- a/frontend/src/ShaderBackground.tsx
+++ b/frontend/src/ShaderBackground.tsx
@@ -3,6 +3,8 @@ import { useEffect, useRef } from "react";
import BLACKHOLE_SHADER_CODE from "./assets/blackhole.glsl?raw";
import STAR_SHADER_CODE from "./assets/star.glsl?raw";
import BALL_SHADER_CODE from "./assets/ball.glsl?raw";
+import REFLECT_BALL_SHADER_CODE from "./assets/reflect.glsl?raw";
+import CLOUDS_SHADER_CODE from "./assets/clouds.glsl?raw";
function buildProgram(
ctx: WebGL2RenderingContext,
@@ -145,6 +147,12 @@ export const ShaderBackground: React.FC = ({
case "ball":
shader_code = BALL_SHADER_CODE;
break;
+ case "reflect":
+ shader_code = REFLECT_BALL_SHADER_CODE;
+ break;
+ case "clouds":
+ shader_code = CLOUDS_SHADER_CODE;
+ break;
default:
console.error("Unknown shader theme:", theme);
return;
diff --git a/frontend/src/assets/clouds.glsl b/frontend/src/assets/clouds.glsl
new file mode 100644
index 0000000..02cf064
--- /dev/null
+++ b/frontend/src/assets/clouds.glsl
@@ -0,0 +1,62 @@
+#version 300 es
+precision highp float;
+uniform vec3 iResolution;
+uniform float iTime;
+
+out vec4 FragColor;
+
+#define T (iTime)
+
+float orb(vec3 p) {
+ // orb time
+ float t = T * 4.;
+ return length(p - vec3(sin(sin(t * .2) + t * .4) * 6., 1. + sin(sin(t * .5) + t * .2) * 4., 12. + T + cos(t * .3) * 8.));
+}
+
+void mainImage(out vec4 o, vec2 u) {
+ float d, a, e, i, s, t = T;
+ vec3 p = iResolution;
+
+ // scale coords
+ u = (u + u - p.xy) / p.y;
+
+ // camera movement
+ u += vec2(cos(t * .1) * .3, cos(t * .3) * .1);
+
+ for(o *= i; i++ < 128.;
+
+ // accumulate distance
+ d += s = min(.03 + .2 * abs(s), e = max(.5 * e, .01)),
+
+ // grayscale color and orb light
+ o += 1. / (s + e * 3.))
+
+ // noise loop start, march
+ for(p = vec3(u * d, d + t), // p = ro + rd *d, p.z + t;
+
+ // entity (orb)
+ e = orb(p) - .1,
+
+ // spin by t, twist by p.z
+ p.xy *= mat2(cos(.1 * t + p.z / 8. + vec4(0, 33, 11, 0))),
+
+ // mirrored planes 4 units apart
+ s = 4. - abs(p.y),
+
+ // noise starts at .8 up to 32., grow by a+=a
+ a = .8; a < 32.; a += a)
+
+ // apply turbulence
+ p += cos(.7 * t + p.yzx) * .2,
+
+ // apply noise
+ s -= abs(dot(sin(.1 * t + p * a), .6 + p - p)) / a;
+
+ // tanh tonemap, brightness, light off-screen
+ o = tanh(o / 1e1);
+}
+
+
+void main() {
+ mainImage(FragColor, gl_FragCoord.xy);
+}
\ No newline at end of file
diff --git a/frontend/src/assets/reflect.glsl b/frontend/src/assets/reflect.glsl
new file mode 100644
index 0000000..d808120
--- /dev/null
+++ b/frontend/src/assets/reflect.glsl
@@ -0,0 +1,375 @@
+#version 300 es
+precision highp float;
+uniform vec3 iResolution;
+uniform float iTime;
+
+out vec4 FragColor;
+
+// CC0: Let's self reflect
+// Always enjoyed the videos of Platonic solids with inner mirrors
+// I made some previous attempts but thought I make another attempt it
+
+// Reducing the alias effects on the inner reflections turned out to be a bit tricky.
+// Simplest solution is just to run run fullscreen on a 4K screen ;)
+
+// Function to generate the solid found here: https://www.shadertoy.com/view/MsKGzw
+
+// Tinker with these parameters to create different solids
+// -------------------------------------------------------
+const float rotation_speed = 0.25f;
+
+const float poly_U = 1.f; // [0, inf]
+const float poly_V = 0.5f; // [0, inf]
+const float poly_W = 1.0f; // [0, inf]
+const int poly_type = 5; // [2, 5]
+const float poly_zoom = 2.5f;
+
+const float inner_sphere = 1.f;
+
+const float refr_index = 0.9f;
+
+#define MAX_BOUNCES2 6
+// -------------------------------------------------------
+
+#define TIME iTime
+#define RESOLUTION iResolution
+#define PI 3.141592654
+#define TAU (2.0*PI)
+
+// License: WTFPL, author: sam hocevar, found: https://stackoverflow.com/a/17897228/418488
+const vec4 hsv2rgb_K = vec4(1.0f, 2.0f / 3.0f, 1.0f / 3.0f, 3.0f);
+vec3 hsv2rgb(vec3 c) {
+ vec3 p = abs(fract(c.xxx + hsv2rgb_K.xyz) * 6.0f - hsv2rgb_K.www);
+ return c.z * mix(hsv2rgb_K.xxx, clamp(p - hsv2rgb_K.xxx, 0.0f, 1.0f), c.y);
+}
+// License: WTFPL, author: sam hocevar, found: https://stackoverflow.com/a/17897228/418488
+// Macro version of above to enable compile-time constants
+#define HSV2RGB(c) (c.z * mix(hsv2rgb_K.xxx, clamp(abs(fract(c.xxx + hsv2rgb_K.xyz) * 6.0 - hsv2rgb_K.www) - hsv2rgb_K.xxx, 0.0, 1.0), c.y))
+
+#define TOLERANCE2 0.0005
+//#define MAX_RAY_LENGTH2 10.0
+#define MAX_RAY_MARCHES2 50
+#define NORM_OFF2 0.005
+#define BACKSTEP2
+
+#define TOLERANCE3 0.0005
+#define MAX_RAY_LENGTH3 10.0
+#define MAX_RAY_MARCHES3 90
+#define NORM_OFF3 0.005
+
+const vec3 rayOrigin = vec3(0.0f, 1.f, -5.f);
+const vec3 sunDir = normalize(-rayOrigin);
+
+const vec3 sunCol = HSV2RGB(vec3(0.06f, 0.90f, 1E-2f)) * 1.f;
+const vec3 bottomBoxCol = HSV2RGB(vec3(0.66f, 0.80f, 0.5f)) * 1.f;
+const vec3 topBoxCol = HSV2RGB(vec3(0.60f, 0.90f, 1.f)) * 1.f;
+const vec3 glowCol0 = HSV2RGB(vec3(0.05f, 0.7f, 1E-3f)) * 1.f;
+const vec3 glowCol1 = HSV2RGB(vec3(0.95f, 0.7f, 1E-3f)) * 1.f;
+const vec3 beerCol = -HSV2RGB(vec3(0.15f + 0.5f, 0.7f, 2.f));
+const float rrefr_index = 1.f / refr_index;
+
+// License: Unknown, author: knighty, found: https://www.shadertoy.com/view/MsKGzw
+const float poly_cospin = cos(PI / float(poly_type));
+const float poly_scospin = sqrt(0.75f - poly_cospin * poly_cospin);
+const vec3 poly_nc = vec3(-0.5f, -poly_cospin, poly_scospin);
+const vec3 poly_pab = vec3(0.f, 0.f, 1.f);
+const vec3 poly_pbc_ = vec3(poly_scospin, 0.f, 0.5f);
+const vec3 poly_pca_ = vec3(0.f, poly_scospin, poly_cospin);
+const vec3 poly_p = normalize((poly_U * poly_pab + poly_V * poly_pbc_ + poly_W * poly_pca_));
+const vec3 poly_pbc = normalize(poly_pbc_);
+const vec3 poly_pca = normalize(poly_pca_);
+
+mat3 g_rot;
+vec2 g_gd;
+
+// License: MIT, author: Inigo Quilez, found: https://iquilezles.org/articles/noacos/
+mat3 rot(vec3 d, vec3 z) {
+ vec3 v = cross(z, d);
+ float c = dot(z, d);
+ float k = 1.0f / (1.0f + c);
+
+ return mat3(v.x * v.x * k + c, v.y * v.x * k - v.z, v.z * v.x * k + v.y, v.x * v.y * k + v.z, v.y * v.y * k + c, v.z * v.y * k - v.x, v.x * v.z * k - v.y, v.y * v.z * k + v.x, v.z * v.z * k + c);
+}
+
+// License: Unknown, author: Matt Taylor (https://github.com/64), found: https://64.github.io/tonemapping/
+vec3 aces_approx(vec3 v) {
+ v = max(v, 0.0f);
+ v *= 0.6f;
+ float a = 2.51f;
+ float b = 0.03f;
+ float c = 2.43f;
+ float d = 0.59f;
+ float e = 0.14f;
+ return clamp((v * (a * v + b)) / (v * (c * v + d) + e), 0.0f, 1.0f);
+}
+
+float sphere(vec3 p, float r) {
+ return length(p) - r;
+}
+
+// License: MIT, author: Inigo Quilez, found: https://iquilezles.org/articles/distfunctions/
+float box(vec2 p, vec2 b) {
+ vec2 d = abs(p) - b;
+ return length(max(d, 0.0f)) + min(max(d.x, d.y), 0.0f);
+}
+
+// License: Unknown, author: knighty, found: https://www.shadertoy.com/view/MsKGzw
+void poly_fold(inout vec3 pos) {
+ vec3 p = pos;
+
+ for(int i = 0; i < poly_type; ++i) {
+ p.xy = abs(p.xy);
+ p -= 2.f * min(0.f, dot(p, poly_nc)) * poly_nc;
+ }
+
+ pos = p;
+}
+
+float poly_plane(vec3 pos) {
+ float d0 = dot(pos, poly_pab);
+ float d1 = dot(pos, poly_pbc);
+ float d2 = dot(pos, poly_pca);
+ float d = d0;
+ d = max(d, d1);
+ d = max(d, d2);
+ return d;
+}
+
+float poly_corner(vec3 pos) {
+ float d = length(pos) - .0125f;
+ return d;
+}
+
+float dot2(vec3 p) {
+ return dot(p, p);
+}
+
+float poly_edge(vec3 pos) {
+ float dla = dot2(pos - min(0.f, pos.x) * vec3(1.f, 0.f, 0.f));
+ float dlb = dot2(pos - min(0.f, pos.y) * vec3(0.f, 1.f, 0.f));
+ float dlc = dot2(pos - min(0.f, dot(pos, poly_nc)) * poly_nc);
+ return sqrt(min(min(dla, dlb), dlc)) - 2E-3f;
+}
+
+vec3 shape(vec3 pos) {
+ pos *= g_rot;
+ pos /= poly_zoom;
+ poly_fold(pos);
+ pos -= poly_p;
+
+ return vec3(poly_plane(pos), poly_edge(pos), poly_corner(pos)) * poly_zoom;
+}
+
+vec3 render0(vec3 ro, vec3 rd) {
+ vec3 col = vec3(0.0f);
+
+ float srd = sign(rd.y);
+ float tp = -(ro.y - 6.f) / abs(rd.y);
+
+ if(srd < 0.f) {
+ col += bottomBoxCol * exp(-0.5f * (length((ro + tp * rd).xz)));
+ }
+
+ if(srd > 0.0f) {
+ vec3 pos = ro + tp * rd;
+ vec2 pp = pos.xz;
+ float db = box(pp, vec2(5.0f, 9.0f)) - 3.0f;
+
+ col += topBoxCol * rd.y * rd.y * smoothstep(0.25f, 0.0f, db);
+ col += 0.2f * topBoxCol * exp(-0.5f * max(db, 0.0f));
+ col += 0.05f * sqrt(topBoxCol) * max(-db, 0.0f);
+ }
+
+ col += sunCol / (1.001f - dot(sunDir, rd));
+ return col;
+}
+
+float df2(vec3 p) {
+ vec3 ds = shape(p);
+ float d2 = ds.y - 5E-3f;
+ float d0 = min(-ds.x, d2);
+ float d1 = sphere(p, inner_sphere);
+ g_gd = min(g_gd, vec2(d2, d1));
+ float d = (min(d0, d1));
+ return d;
+}
+
+float rayMarch2(vec3 ro, vec3 rd, float tinit) {
+ float t = tinit;
+#if defined(BACKSTEP2)
+ vec2 dti = vec2(1e10f, 0.0f);
+#endif
+ int i;
+ for(i = 0; i < MAX_RAY_MARCHES2; ++i) {
+ float d = df2(ro + rd * t);
+#if defined(BACKSTEP2)
+ if(d < dti.x) {
+ dti = vec2(d, t);
+ }
+#endif
+ // Bouncing in a closed shell, will never miss
+ if(d < TOLERANCE2/* || t > MAX_RAY_LENGTH3 */) {
+ break;
+ }
+ t += d;
+ }
+#if defined(BACKSTEP2)
+ if(i == MAX_RAY_MARCHES2) {
+ t = dti.y;
+ };
+#endif
+ return t;
+}
+
+vec3 normal2(vec3 pos) {
+ vec2 eps = vec2(NORM_OFF2, 0.0f);
+ vec3 nor;
+ nor.x = df2(pos + eps.xyy) - df2(pos - eps.xyy);
+ nor.y = df2(pos + eps.yxy) - df2(pos - eps.yxy);
+ nor.z = df2(pos + eps.yyx) - df2(pos - eps.yyx);
+ return normalize(nor);
+}
+
+vec3 render2(vec3 ro, vec3 rd, float db) {
+ vec3 agg = vec3(0.0f);
+ float ragg = 1.f;
+ float tagg = 0.f;
+
+ for(int bounce = 0; bounce < MAX_BOUNCES2; ++bounce) {
+ if(ragg < 0.1f)
+ break;
+ g_gd = vec2(1E3f);
+ float t2 = rayMarch2(ro, rd, min(db + 0.05f, 0.3f));
+ vec2 gd2 = g_gd;
+ tagg += t2;
+
+ vec3 p2 = ro + rd * t2;
+ vec3 n2 = normal2(p2);
+ vec3 r2 = reflect(rd, n2);
+ vec3 rr2 = refract(rd, n2, rrefr_index);
+ float fre2 = 1.f + dot(n2, rd);
+
+ vec3 beer = ragg * exp(0.2f * beerCol * tagg);
+ agg += glowCol1 * beer * ((1.f + tagg * tagg * 4E-2f) * 6.f / max(gd2.x, 5E-4f + tagg * tagg * 2E-4f / ragg));
+ vec3 ocol = 0.2f * beer * render0(p2, rr2);
+ if(gd2.y <= TOLERANCE2) {
+ ragg *= 1.f - 0.9f * fre2;
+ } else {
+ agg += ocol;
+ ragg *= 0.8f;
+ }
+
+ ro = p2;
+ rd = r2;
+ db = gd2.x;
+ }
+
+ return agg;
+}
+
+float df3(vec3 p) {
+ vec3 ds = shape(p);
+ g_gd = min(g_gd, ds.yz);
+ const float sw = 0.02f;
+ float d1 = min(ds.y, ds.z) - sw;
+ float d0 = ds.x;
+ d0 = min(d0, ds.y);
+ d0 = min(d0, ds.z);
+ return d0;
+}
+
+float rayMarch3(vec3 ro, vec3 rd, float tinit, out int iter) {
+ float t = tinit;
+ int i;
+ for(i = 0; i < MAX_RAY_MARCHES3; ++i) {
+ float d = df3(ro + rd * t);
+ if(d < TOLERANCE3 || t > MAX_RAY_LENGTH3) {
+ break;
+ }
+ t += d;
+ }
+ iter = i;
+ return t;
+}
+
+vec3 normal3(vec3 pos) {
+ vec2 eps = vec2(NORM_OFF3, 0.0f);
+ vec3 nor;
+ nor.x = df3(pos + eps.xyy) - df3(pos - eps.xyy);
+ nor.y = df3(pos + eps.yxy) - df3(pos - eps.yxy);
+ nor.z = df3(pos + eps.yyx) - df3(pos - eps.yyx);
+ return normalize(nor);
+}
+
+vec3 render3(vec3 ro, vec3 rd) {
+ int iter;
+
+ vec3 skyCol = render0(ro, rd);
+ vec3 col = skyCol;
+
+ g_gd = vec2(1E3f);
+ float t1 = rayMarch3(ro, rd, 0.1f, iter);
+ vec2 gd1 = g_gd;
+ vec3 p1 = ro + t1 * rd;
+ vec3 n1 = normal3(p1);
+ vec3 r1 = reflect(rd, n1);
+ vec3 rr1 = refract(rd, n1, refr_index);
+ float fre1 = 1.f + dot(rd, n1);
+ fre1 *= fre1;
+
+ float ifo = mix(0.5f, 1.f, smoothstep(1.0f, 0.9f, float(iter) / float(MAX_RAY_MARCHES3)));
+
+ if(t1 < MAX_RAY_LENGTH3) {
+ col = render0(p1, r1) * (0.5f + 0.5f * fre1) * ifo;
+ vec3 icol = render2(p1, rr1, gd1.x);
+ if(gd1.x > TOLERANCE3 && gd1.y > TOLERANCE3 && rr1 != vec3(0.f)) {
+ col += icol * (1.f - 0.75f * fre1) * ifo;
+ }
+ }
+
+ col += (glowCol0 + 1.f * fre1 * (glowCol0)) / max(gd1.x, 3E-4f);
+ return col;
+
+}
+
+vec3 effect(vec2 p, vec2 pp) {
+ const float fov = 2.0f;
+
+ const vec3 up = vec3(0.f, 1.f, 0.f);
+ const vec3 la = vec3(0.0f);
+
+ const vec3 ww = normalize(normalize(la - rayOrigin));
+ const vec3 uu = normalize(cross(up, ww));
+ const vec3 vv = cross(ww, uu);
+
+ vec3 rd = normalize(-p.x * uu + p.y * vv + fov * ww);
+
+ vec3 col = vec3(0.0f);
+ col = render3(rayOrigin, rd);
+
+ col -= 2E-2f * vec3(2.f, 3.f, 1.f) * (length(p) + 0.25f);
+ col = aces_approx(col);
+ col = sqrt(col);
+ return col;
+}
+
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 q = fragCoord / RESOLUTION.xy;
+ vec2 p = -1.f + 2.f * q;
+ vec2 pp = p;
+ p.x *= RESOLUTION.x / RESOLUTION.y;
+
+ float a = TIME * rotation_speed;
+ vec3 r0 = vec3(1.0f, sin(vec2(sqrt(0.5f), 1.0f) * a));
+ vec3 r1 = vec3(cos(vec2(sqrt(0.5f), 1.0f) * 0.913f * a), 1.0f);
+ mat3 rot = rot(normalize(r0), normalize(r1));
+ g_rot = rot;
+
+ vec3 col = effect(p, pp);
+
+ fragColor = vec4(col, 1.0f);
+}
+
+void main() {
+ mainImage(FragColor, gl_FragCoord.xy);
+}
\ No newline at end of file