diff --git a/src/components/base/BackgroundImage.tsx b/src/components/base/BackgroundImage.tsx index be55be8..5dccb81 100644 --- a/src/components/base/BackgroundImage.tsx +++ b/src/components/base/BackgroundImage.tsx @@ -11,6 +11,8 @@ import planet from '@/components/themes/planet' import random from '@/components/themes/random' import tunnel from '@/components/themes/tunnel' import contour from '@/components/themes/contour' +import rings3d from '@/components/themes/rings3d' +import zappy from '@/components/themes/zappy' const themeMap: Record = { flow, @@ -23,6 +25,8 @@ const themeMap: Record = { random, tunnel, contour, + rings3d, + zappy, } export default defineComponent({ diff --git a/src/components/themes/rings3d/fragment.glsl b/src/components/themes/rings3d/fragment.glsl new file mode 100644 index 0000000..b472d74 --- /dev/null +++ b/src/components/themes/rings3d/fragment.glsl @@ -0,0 +1,150 @@ +// Inspiration: https://www.shadertoy.com/view/WdB3Dw +#ifdef GL_ES +precision highp float; +#endif + +uniform float uTime; +uniform vec2 uResolution; + +// Torus tube cross-section radius — thicker values widen the ring walls +uniform float uTubeSize; + +// Clipping sphere radius — controls how large the bubble appears +uniform float uBubbleSize; + +// Ray march step fraction — lower = slower rays, denser glow/fog accumulation +uniform float uFogDensity; + +// Spectrum cycling frequency — higher = more colour bands visible per ray +uniform float uSpectrumSpeed; + +// Surface glow colour (normalised; internally scaled ×2.1 to allow overbright) +uniform vec3 uGlowColor; + +// Ambient scatter/fog colour accumulated on every ray march step +uniform vec3 uFogColor; + +#define PI 3.14159265359 + +void pR(inout vec2 p, float a) { + p = cos(a)*p + sin(a)*vec2(p.y, -p.x); +} + +float smax(float a, float b, float r) { + vec2 u = max(vec2(r + a, r + b), vec2(0.0)); + return min(-r, max(a, b)) + length(u); +} + +// ─── Spectrum palette ──────────────────────────────────────────────────────── +// Cosine colour palette — IQ https://www.shadertoy.com/view/ll2GD3 + +vec3 pal(in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d) { + return a + b*cos(6.28318*(c*t+d)); +} + +vec3 spectrum(float n) { + return pal(n, vec3(0.5,0.5,0.5), vec3(0.5,0.5,0.5), + vec3(1.0,1.0,1.0), vec3(0.0,0.33,0.67)); +} + +// ─── SDF via inverse stereographic projection ──────────────────────────────── + +vec4 inverseStereographic(vec3 p, out float k) { + k = 2.0 / (1.0 + dot(p, p)); + return vec4(k*p, k - 1.0); +} + +float fTorus(vec4 p4) { + float d1 = length(p4.xy) / length(p4.zw) - 1.0; + float d2 = length(p4.zw) / length(p4.xy) - 1.0; + float d = d1 < 0.0 ? -d1 : d2; + d /= PI; + return d; +} + +float fixDistance(float d, float k) { + float sn = sign(d); + d = abs(d); + d = d / k * 1.82; + d += 1.0; + d = pow(d, 0.5); + d -= 1.0; + d *= 5.0 / 3.0; + d *= sn; + return d; +} + +float gTime; + +float map(vec3 p) { + float k; + vec4 p4 = inverseStereographic(p, k); + pR(p4.zy, gTime * -PI / 2.0); + pR(p4.xw, gTime * -PI / 2.0); + float d = fTorus(p4); + d = abs(d); + d -= uTubeSize; + d = fixDistance(d, k); + d = smax(d, length(p) - uBubbleSize, 0.2); + return d; +} + +// ─── Rendering ─────────────────────────────────────────────────────────────── + +mat3 calcLookAtMatrix(vec3 ro, vec3 ta, vec3 up) { + vec3 ww = normalize(ta - ro); + vec3 uu = normalize(cross(ww, up)); + vec3 vv = normalize(cross(uu, ww)); + return mat3(uu, vv, ww); +} + +void main() { + gTime = mod(uTime / 2.0, 1.0); + + vec3 camPos = vec3(1.8, 5.5, -5.5) * 1.75; + vec3 camTar = vec3(0.0, 0.0, 0.0); + vec3 camUp = vec3(-1.0, 0.0, -1.5); + mat3 camMat = calcLookAtMatrix(camPos, camTar, camUp); + + float focalLength = 5.0; + vec2 p = (-uResolution.xy + 2.0 * gl_FragCoord.xy) / uResolution.y; + + vec3 rayDir = normalize(camMat * vec3(p, focalLength)); + vec3 rayPos = camPos; + float rayLen = 0.0; + float dist = 0.0; + vec3 color = vec3(0.0); + + const int ITER = 82; + const float MIN_STEP = 0.001; + const float MAX_DIST = 20.0; + + for (int i = 0; i < ITER; i++) { + rayLen += max(MIN_STEP, abs(dist) * uFogDensity); + rayPos = camPos + rayDir * rayLen; + dist = map(rayPos); + + // Surface glow: bright flare near the SDF surface, tinted by uGlowColor + vec3 c = vec3(max(0.0, 0.01 - abs(dist)) * 0.5); + c *= uGlowColor * 2.1; // ×2.1 restores the original overbright range + + // Ambient fog: coloured scatter accumulated on every step + c += uFogColor * uFogDensity / 160.0; + + c *= smoothstep(20.0, 7.0, length(rayPos)); + + float rl = smoothstep(MAX_DIST, 0.1, rayLen); + c *= rl; + c *= spectrum(rl * uSpectrumSpeed - 0.6); + + color += c; + + if (rayLen > MAX_DIST) break; + } + + color = pow(color, vec3(1.0 / 1.8)) * 2.0; + color = pow(color, vec3(2.0)) * 3.0; + color = pow(color, vec3(1.0 / 2.2)); + + gl_FragColor = vec4(color, 1.0); +} diff --git a/src/components/themes/rings3d/index.tsx b/src/components/themes/rings3d/index.tsx new file mode 100644 index 0000000..06db0f1 --- /dev/null +++ b/src/components/themes/rings3d/index.tsx @@ -0,0 +1,75 @@ +import { defineComponent, ref, computed, onMounted, onBeforeUnmount } from 'vue' +import GL from '@/utils/gl' +import { useThemeStore } from '@/store/theme' +import { hexToRgb } from '@/utils/color' +import fragmentShader from './fragment.glsl' +import vertexShader from './vertex.glsl' + +let render: GL + +export default defineComponent({ + name: 'Rings3dTheme', + setup() { + const themeStore = useThemeStore() + const canvasRef = ref(null) + + const animationSpeed = computed(() => themeStore.getThemeInput('animation-speed')?.value as number || 5) + const pxratio = computed(() => themeStore.getThemeInput('pxratio')?.value as number || 0.8) + const tubeSize = computed(() => themeStore.getThemeInput('tube-size')?.value as number || 0.2) + const bubbleSize = computed(() => themeStore.getThemeInput('bubble-size')?.value as number || 1.85) + const fogDensity = computed(() => themeStore.getThemeInput('fog-density')?.value as number || 0.8) + const spectrumSpeed = computed(() => themeStore.getThemeInput('spectrum-speed')?.value as number || 6.0) + const glowColor = computed(() => themeStore.getThemeInput('color-glow')?.value as string || '#AAFFCE') + const fogColor = computed(() => themeStore.getThemeInput('color-fog')?.value as string || '#9940B3') + + onMounted(() => { + render = new GL( + canvasRef.value!, + vertexShader, + fragmentShader, + window.innerWidth, + window.innerHeight, + { + renderOptions: { externalTimeUse: true }, + renderHook() { + const gl = this as unknown as GL + + if (!gl.programInfo.uniforms.tubeSize) { + gl.programInfo.uniforms.tubeSize = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uTubeSize') + gl.programInfo.uniforms.bubbleSize = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uBubbleSize') + gl.programInfo.uniforms.fogDensity = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uFogDensity') + gl.programInfo.uniforms.spectrumSpeed = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uSpectrumSpeed') + gl.programInfo.uniforms.glowColor = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uGlowColor') + gl.programInfo.uniforms.fogColor = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uFogColor') + } + + gl.pxratio = pxratio.value + gl.time += animationSpeed.value / 500 + + const gc = hexToRgb(glowColor.value) + const fc = hexToRgb(fogColor.value) + + gl.ctx.uniform1f(gl.programInfo.uniforms.time, gl.time) + gl.ctx.uniform1f(gl.programInfo.uniforms.tubeSize, tubeSize.value) + gl.ctx.uniform1f(gl.programInfo.uniforms.bubbleSize, bubbleSize.value) + gl.ctx.uniform1f(gl.programInfo.uniforms.fogDensity, fogDensity.value) + gl.ctx.uniform1f(gl.programInfo.uniforms.spectrumSpeed, spectrumSpeed.value) + gl.ctx.uniform3f(gl.programInfo.uniforms.glowColor, gc[0] / 255, gc[1] / 255, gc[2] / 255) + gl.ctx.uniform3f(gl.programInfo.uniforms.fogColor, fc[0] / 255, fc[1] / 255, fc[2] / 255) + } + } + ) + render.running = true + }) + + onBeforeUnmount(() => { + render.running = false + }) + + return () => ( +
+ +
+ ) + } +}) diff --git a/src/components/themes/rings3d/vertex.glsl b/src/components/themes/rings3d/vertex.glsl new file mode 100644 index 0000000..9d68485 --- /dev/null +++ b/src/components/themes/rings3d/vertex.glsl @@ -0,0 +1,5 @@ +attribute vec2 aVertexPosition; + +void main() { + gl_Position = vec4(aVertexPosition, 0.0, 1.0); +} diff --git a/src/components/themes/zappy/fragment.glsl b/src/components/themes/zappy/fragment.glsl new file mode 100644 index 0000000..230ad24 --- /dev/null +++ b/src/components/themes/zappy/fragment.glsl @@ -0,0 +1,77 @@ +// Inspiration: https://www.shadertoy.com/view/cfjGzV — Zippy Zaps +// Credits: -13 thanks to Nguyen2007 ⚡ +#ifdef GL_ES +precision highp float; +#endif + +uniform float uTime; +uniform vec2 uResolution; + +// Overall zoom / scale of the electric pattern +uniform float uZoom; + +// Shifts the base colour phase vector, cycling through hue combinations +uniform float uColorShift; + +// tanh is GLSL ES 3.00+ only — not available in WebGL 1.0. +// Implemented via the identity tanh(x) = (e^2x - 1)/(e^2x + 1). +// Input clamped to ±15 to prevent exp() overflow on large inputs. +vec2 tanh(vec2 x) { + x = clamp(x, -15.0, 15.0); + vec2 e = exp(2.0 * x); + return (e - 1.0) / (e + 1.0); +} + +void main() { + vec2 res = uResolution; + vec2 u = gl_FragCoord.xy; + + // Centre and normalise to screen height; uZoom replaces the hardcoded 0.2 + u = uZoom * (u + u - res) / res.y; + + // Base colour phase vector — uColorShift scrolls through hue space + // z.wxzw swizzle = vec4(z.w, z.x, z.z, z.w) = vec4(0,1,3,0) used in mat2 rotation + vec4 z = vec4(1.0 + uColorShift, 2.0 + uColorShift, 3.0 + uColorShift, 0.0); + vec4 o = z; + + float a = 0.5; + float t = uTime; + float i = 0.0; + + // Original: for (float a=.5, t=iTime, i; ++i<19.; o+=...) v=..., u+=... + // Rewritten: float i counts 1..18 (pre-increment in condition); + // ++t and a+=.03 moved to explicit statements preserving order; + // comma-operator body split into sequential statements. + vec2 v = res; + + for (int iter = 0; iter < 18; iter++) { + i += 1.0; // pre-increment: matches ++i in original condition + + // ++t happens inside the v= expression (pre-increment before the rhs is evaluated) + t += 1.0; + a += 0.03; + + v = cos(t - 7.0 * u * pow(a, i)) - 5.0 * u; + + // mat2 from explicit components avoids mat2(vec4) constructor rejection on ANGLE/Mesa + // vec4(scalar) - vec4 ensures float-vec4 subtraction is unambiguous + vec4 r = cos(vec4(i + 0.02 * t) - z.wxzw * 11.0); + u *= mat2(r.x, r.y, r.z, r.w); + + // If black/NaN artifacts appear, replace tanh with stanh (smoothstep-tanh approximation) + u += tanh(40.0 * dot(u, u) * cos(100.0 * u.yx + t)) / 200.0 + + 0.2 * a * u + + cos(4.0 / exp(dot(o, o) / 100.0) + t) / 300.0; + + // for-increment: accumulate glow using the freshly updated v and u + o += (1.0 + cos(z + t)) + / length((1.0 + i * dot(v, v)) + * sin(1.5 * u / (0.5 - dot(u, u)) - 9.0 * u.yx + t)); + } + + // Tonemapping: multiply numerator/denominator by o to avoid float/vec4 division + // Equivalent to: 25.6 / (min(o,13.) + 164./o) + o = 25.6 * o / (min(o, 13.0) * o + 164.0) - dot(u, u) / 250.0; + + gl_FragColor = vec4(o); +} diff --git a/src/components/themes/zappy/index.tsx b/src/components/themes/zappy/index.tsx new file mode 100644 index 0000000..111aacf --- /dev/null +++ b/src/components/themes/zappy/index.tsx @@ -0,0 +1,59 @@ +import { defineComponent, ref, computed, onMounted, onBeforeUnmount } from 'vue' +import GL from '@/utils/gl' +import { useThemeStore } from '@/store/theme' +import fragmentShader from './fragment.glsl' +import vertexShader from './vertex.glsl' + +let render: GL + +export default defineComponent({ + name: 'ZappyTheme', + setup() { + const themeStore = useThemeStore() + const canvasRef = ref(null) + + const animationSpeed = computed(() => themeStore.getThemeInput('animation-speed')?.value as number || 5) + const pxratio = computed(() => themeStore.getThemeInput('pxratio')?.value as number || 0.8) + const zoom = computed(() => themeStore.getThemeInput('zoom')?.value as number || 0.2) + const colorShift = computed(() => themeStore.getThemeInput('color-shift')?.value as number || 0.0) + + onMounted(() => { + render = new GL( + canvasRef.value!, + vertexShader, + fragmentShader, + window.innerWidth, + window.innerHeight, + { + renderOptions: { externalTimeUse: true }, + renderHook() { + const gl = this as unknown as GL + + if (!gl.programInfo.uniforms.zoom) { + gl.programInfo.uniforms.zoom = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uZoom') + gl.programInfo.uniforms.colorShift = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uColorShift') + } + + gl.pxratio = pxratio.value + gl.time += animationSpeed.value / 500 + + gl.ctx.uniform1f(gl.programInfo.uniforms.time, gl.time) + gl.ctx.uniform1f(gl.programInfo.uniforms.zoom, zoom.value) + gl.ctx.uniform1f(gl.programInfo.uniforms.colorShift, colorShift.value) + } + } + ) + render.running = true + }) + + onBeforeUnmount(() => { + render.running = false + }) + + return () => ( +
+ +
+ ) + } +}) diff --git a/src/components/themes/zappy/vertex.glsl b/src/components/themes/zappy/vertex.glsl new file mode 100644 index 0000000..9d68485 --- /dev/null +++ b/src/components/themes/zappy/vertex.glsl @@ -0,0 +1,5 @@ +attribute vec2 aVertexPosition; + +void main() { + gl_Position = vec4(aVertexPosition, 0.0, 1.0); +} diff --git a/src/locales/de.json b/src/locales/de.json index 3ad1cac..c969df8 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -33,7 +33,14 @@ "glow": "Leuchten", "glow-width": "Leuchtbreite", "max-line": "Max. Linie", - "color-bg": "Hintergrund" + "color-bg": "Hintergrund", + "tube-size": "Rohrdicke", + "bubble-size": "Blasengröße", + "fog-density": "Nebeldichte", + "spectrum-speed": "Spektralgeschwindigkeit", + "color-glow": "Leuchtfarbe", + "color-fog": "Nebelfarbe", + "color-shift": "Farbversatz" }, "text": { "password": "Passwort", diff --git a/src/locales/en.json b/src/locales/en.json index 2a32eaf..6f89431 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -33,7 +33,14 @@ "glow": "Glow", "glow-width": "Glow Width", "max-line": "Max Line", - "color-bg": "Background" + "color-bg": "Background", + "tube-size": "Tube Size", + "bubble-size": "Bubble Size", + "fog-density": "Fog Density", + "spectrum-speed": "Spectrum Speed", + "color-glow": "Glow Color", + "color-fog": "Fog Color", + "color-shift": "Color Shift" }, "text": { "password": "password", diff --git a/src/locales/es.json b/src/locales/es.json index f0ad188..5349f11 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -33,7 +33,14 @@ "glow": "Resplandor", "glow-width": "Ancho del halo", "max-line": "Línea máx.", - "color-bg": "Fondo" + "color-bg": "Fondo", + "tube-size": "Grosor del tubo", + "bubble-size": "Tamaño de burbuja", + "fog-density": "Densidad de niebla", + "spectrum-speed": "Velocidad del espectro", + "color-glow": "Color del brillo", + "color-fog": "Color de la niebla", + "color-shift": "Desplazamiento de color" }, "text": { "password": "contraseña", diff --git a/src/locales/fr.json b/src/locales/fr.json index 0067214..d48b267 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -33,7 +33,14 @@ "glow": "Lueur", "glow-width": "Largeur du halo", "max-line": "Ligne max", - "color-bg": "Arrière-plan" + "color-bg": "Arrière-plan", + "tube-size": "Épaisseur du tube", + "bubble-size": "Taille de la bulle", + "fog-density": "Densité du brouillard", + "spectrum-speed": "Vitesse du spectre", + "color-glow": "Couleur du halo", + "color-fog": "Couleur du brouillard", + "color-shift": "Décalage couleur" }, "text": { "password": "mot de passe", diff --git a/src/locales/ru.json b/src/locales/ru.json index 8d597ef..1941e37 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -33,7 +33,14 @@ "glow": "Свечение", "glow-width": "Ширина свечения", "max-line": "Макс. линия", - "color-bg": "Фон" + "color-bg": "Фон", + "tube-size": "Размер трубки", + "bubble-size": "Размер пузыря", + "fog-density": "Плотность тумана", + "spectrum-speed": "Скорость спектра", + "color-glow": "Цвет свечения", + "color-fog": "Цвет тумана", + "color-shift": "Сдвиг цвета" }, "text": { "password": "пароль", diff --git a/src/utils/constant.ts b/src/utils/constant.ts index bc64a7b..8a32aaa 100644 --- a/src/utils/constant.ts +++ b/src/utils/constant.ts @@ -167,6 +167,38 @@ export const AppThemes: AppTheme[] = [ resetButton ] }, + { + name: 'Rings3D', + component: 'rings3d', + color: { + background: '#0a0015' + }, + settings: [ + pxratio(), + buildInputSlider({ value: 5, min: 0, max: 15, step: 0.1 }), + buildInputSlider({ name: 'tube-size', value: 0.2, min: 0.02, max: 0.6, step: 0.01 }), + buildInputSlider({ name: 'bubble-size', value: 1.85, min: 0.5, max: 3.5, step: 0.05 }), + buildInputSlider({ name: 'fog-density', value: 0.8, min: 0.2, max: 1.0, step: 0.01 }), + buildInputSlider({ name: 'spectrum-speed', value: 6.0, min: 1.0, max: 12.0, step: 0.5 }), + buildInputColor({ name: 'color-glow', value: '#AAFFCE' }), + buildInputColor({ name: 'color-fog', value: '#9940B3' }), + randomButton, + resetButton + ] + }, + { + name: 'Zappy', + component: 'zappy', + color: { background: '#000010' }, + settings: [ + pxratio(), + buildInputSlider({ value: 5, min: 0, max: 15, step: 0.1 }), + buildInputSlider({ name: 'zoom', value: 0.2, min: 0.05, max: 0.6, step: 0.01 }), + buildInputSlider({ name: 'color-shift', value: 0, min: 0, max: 6.28, step: 0.05 }), + randomButton, + resetButton + ] + }, { name: 'Contour', component: 'contour',