diff --git a/FractalVisualizer/Game1.cs b/FractalVisualizer/Game1.cs index 2466a6f..df62bf1 100644 --- a/FractalVisualizer/Game1.cs +++ b/FractalVisualizer/Game1.cs @@ -124,7 +124,7 @@ public class Game1 : Game if (IsPressed(keyboard, Keys.Right)) { - _fractalPresetIndex = Math.Min(8, _fractalPresetIndex + 1); + _fractalPresetIndex = Math.Min(19, _fractalPresetIndex + 1); } if (IsPressed(keyboard, Keys.M)) @@ -212,6 +212,17 @@ public class Game1 : Game 6 => "FLAMEGRILL", 7 => "NOTES", 8 => "FIREWORKS", + 9 => "EQCITY", + 10 => "METABALL", + 11 => "GALAXY", + 12 => "AURORA", + 13 => "VORTEX", + 14 => "LASERGRID", + 15 => "RAINRIPPLE", + 16 => "INKSMOKE", + 17 => "MOSAIC", + 18 => "RIBBON", + 19 => "GRAINBARS", _ => "JULIA" }; } diff --git a/FractalVisualizer/Rendering/FractalRenderer.cs b/FractalVisualizer/Rendering/FractalRenderer.cs index c496db9..f65a959 100644 --- a/FractalVisualizer/Rendering/FractalRenderer.cs +++ b/FractalVisualizer/Rendering/FractalRenderer.cs @@ -72,6 +72,39 @@ public sealed class FractalRenderer : IDisposable case 8: RenderFireworks(features, timeSeconds); break; + case 9: + RenderEqualizerCity(features, timeSeconds); + break; + case 10: + RenderLiquidMetaballs(features, timeSeconds); + break; + case 11: + RenderParticleGalaxy(features, timeSeconds); + break; + case 12: + RenderAuroraCurtains(features, timeSeconds); + break; + case 13: + RenderVortexPortal(features, timeSeconds); + break; + case 14: + RenderLaserGrid(features, timeSeconds); + break; + case 15: + RenderRainRipple(features, timeSeconds); + break; + case 16: + RenderInkSmoke(features, timeSeconds); + break; + case 17: + RenderMirrorMosaic(features, timeSeconds); + break; + case 18: + RenderWaveformRibbon(features, timeSeconds); + break; + case 19: + RenderGrainBars(features, timeSeconds); + break; default: RenderJulia(features, timeSeconds); break; @@ -92,10 +125,14 @@ public sealed class FractalRenderer : IDisposable float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); + float lowSpec = GetSpectrumBand(features, 0.00f, 0.20f); + float midSpec = GetSpectrumBand(features, 0.20f, 0.60f); + float highSpec = GetSpectrumBand(features, 0.60f, 1.00f); + float fullSpec = Clamp01((lowSpec + midSpec + highSpec) / 3f); - int shiftPixels = 1 + (int)MathF.Round((high * 2f) + (beat * 2f)); - float trailStrength = Clamp01(0.16f + (bass * 0.28f) + (beat * 0.34f)); - float glowBoost = (high * 0.08f) + (beat * 0.06f); + int shiftPixels = 1 + (int)MathF.Round((high * 2f) + (highSpec * 3f) + (beat * 2f)); + float trailStrength = Clamp01(0.14f + (bass * 0.20f) + (lowSpec * 0.20f) + (beat * 0.26f)); + float glowBoost = (high * 0.05f) + (highSpec * 0.08f) + (midSpec * 0.05f) + (beat * 0.05f); for (int y = 0; y < _internalHeight; y++) { @@ -122,9 +159,18 @@ public sealed class FractalRenderer : IDisposable float nx = (x / (_internalWidth - 1f)) * 2f - 1f; float vignette = Clamp01(1f - ((nx * nx + ny * ny) * 0.42f)); + float spectralIndex = ((x / (float)_internalWidth) * 63f) + (timeSeconds * 6f) + (y * 0.03f); + float spectralPulse = GetSpectrumSample(features, spectralIndex); + float hueWarpR = 1f + (lowSpec * 0.20f) + spectralPulse * 0.12f; + float hueWarpG = 1f + (midSpec * 0.24f) + spectralPulse * 0.08f; + float hueWarpB = 1f + (highSpec * 0.28f) + spectralPulse * 0.14f; crt *= scanline; crt *= 0.5f + (vignette * 0.5f); crt += raw * glowBoost; + crt.X *= hueWarpR; + crt.Y *= hueWarpG; + crt.Z *= hueWarpB; + crt += new Vector3(spectralPulse * 0.03f, spectralPulse * 0.02f, spectralPulse * 0.035f) * (0.3f + fullSpec); crt.X = Clamp01(crt.X); crt.Y = Clamp01(crt.Y); @@ -137,6 +183,32 @@ public sealed class FractalRenderer : IDisposable } } + private float GetSpectrumBand(AudioFeatures features, float normalizedStart, float normalizedEnd) + { + if (features.Spectrum is null || features.Spectrum.Length == 0) + { + return 0f; + } + + int length = features.Spectrum.Length; + int start = Math.Clamp((int)(normalizedStart * length), 0, length - 1); + int end = Math.Clamp((int)(normalizedEnd * length), start + 1, length); + float sum = 0f; + int count = 0; + for (int i = start; i < end; i++) + { + sum += features.Spectrum[i]; + count++; + } + + if (count == 0) + { + return 0f; + } + + return Clamp01((sum / count) * 0.08f); + } + private void RenderMandelbulbStyle(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); @@ -295,6 +367,22 @@ public sealed class FractalRenderer : IDisposable return 0.5f * MathF.Log(r + 1e-6f) * r / dr; } + private static float Frac(float v) + { + return v - MathF.Floor(v); + } + + private static float Hash01(int x, int y, int seed) + { + unchecked + { + int h = x * 374761393 + y * 668265263 + seed * 2147483647; + h = (h ^ (h >> 13)) * 1274126177; + h ^= h >> 16; + return (h & 0x7fffffff) / (float)int.MaxValue; + } + } + private void RenderFlame(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); @@ -622,6 +710,358 @@ public sealed class FractalRenderer : IDisposable } } + private void RenderEqualizerCity(AudioFeatures features, float timeSeconds) + { + for (int y = 0; y < _internalHeight; y++) + { + float t = y / (float)(_internalHeight - 1); + Color bg = new(0.01f, 0.02f + 0.08f * (1f - t), 0.08f + 0.18f * (1f - t), 1f); + int row = y * _internalWidth; + for (int x = 0; x < _internalWidth; x++) + { + _pixelBuffer[row + x] = bg; + } + } + + int buildings = 48; + float beat = Clamp01(features.Beat); + for (int i = 0; i < buildings; i++) + { + float bin = GetSpectrumSample(features, i * (64f / buildings)); + int bw = Math.Max(2, _internalWidth / buildings); + int x0 = i * bw; + int h = (int)((_internalHeight * 0.15f) + bin * _internalHeight * 0.75f); + int y0 = _internalHeight - h; + Vector3 c = new(0.08f + bin * 0.9f, 0.2f + bin * 0.6f + beat * 0.2f, 0.4f + bin * 0.5f); + DrawRect(x0, y0, bw - 1, h, c, 0.95f); + + for (int wy = y0 + 4; wy < _internalHeight - 2; wy += 6) + { + for (int wx = x0 + 1; wx < x0 + bw - 1; wx += 4) + { + if (Hash01(wx, wy, i) > 0.55f - beat * 0.25f) + { + DrawRect(wx, wy, 2, 2, new Vector3(1f, 0.85f, 0.4f), 0.75f); + } + } + } + } + } + + private void RenderLiquidMetaballs(AudioFeatures features, float timeSeconds) + { + float bass = Clamp01(features.Bass * 0.012f); + float high = Clamp01(features.High * 0.003f); + for (int y = 0; y < _internalHeight; y++) + { + float py = y / (float)_internalHeight; + for (int x = 0; x < _internalWidth; x++) + { + float px = x / (float)_internalWidth; + float field = 0f; + for (int i = 0; i < 6; i++) + { + float ax = 0.5f + MathF.Sin(timeSeconds * (0.4f + i * 0.13f) + i) * (0.2f + bass * 0.1f); + float ay = 0.5f + MathF.Cos(timeSeconds * (0.35f + i * 0.11f) + i * 1.7f) * (0.25f + bass * 0.12f); + float dx = px - ax; + float dy = py - ay; + field += 0.02f / (dx * dx + dy * dy + 0.002f); + } + + float m = Clamp01((field - 0.35f) * 0.8f); + float edge = Clamp01((field - 0.5f) * 2.5f); + float r = Clamp01(m * 0.2f + edge * (0.5f + high)); + float g = Clamp01(m * 0.45f + edge * 0.3f); + float b = Clamp01(m * 0.75f + edge * (0.2f + high)); + _pixelBuffer[y * _internalWidth + x] = new Color(r, g, b, 1f); + } + } + } + + private void RenderParticleGalaxy(AudioFeatures features, float timeSeconds) + { + for (int i = 0; i < _pixelBuffer.Length; i++) + { + _pixelBuffer[i] = new Color(0.01f, 0.01f, 0.04f, 1f); + } + + float beat = Clamp01(features.Beat); + int particles = 2200; + float arms = 4f + Clamp01(features.Mid * 0.002f) * 3f; + for (int i = 0; i < particles; i++) + { + float fi = i / (float)particles; + float radius = MathF.Pow(fi, 0.65f) * 0.48f * (1f + beat * 0.2f); + float arm = (i % (int)arms) / arms; + float angle = arm * MathF.Tau + radius * 14f + timeSeconds * (0.15f + beat * 0.8f); + float x = 0.5f + MathF.Cos(angle) * radius; + float y = 0.5f + MathF.Sin(angle) * radius; + int px = (int)(x * _internalWidth); + int py = (int)(y * _internalHeight); + if (px < 0 || py < 0 || px >= _internalWidth || py >= _internalHeight) + { + continue; + } + + float hue = fi * 0.7f + beat * 0.2f; + Vector3 c = new( + 0.5f + 0.5f * MathF.Sin(hue * 6.28f), + 0.5f + 0.5f * MathF.Sin(hue * 6.28f + 2.1f), + 0.5f + 0.5f * MathF.Sin(hue * 6.28f + 4.2f)); + DrawFilledCircle(px, py, 1, c, 0.85f); + } + + DrawFilledCircle(_internalWidth / 2, _internalHeight / 2, 6, new Vector3(1f, 0.9f, 0.7f), 0.9f); + } + + private void RenderAuroraCurtains(AudioFeatures features, float timeSeconds) + { + float mid = Clamp01(features.Mid * 0.003f); + float high = Clamp01(features.High * 0.003f); + for (int y = 0; y < _internalHeight; y++) + { + float ny = y / (float)(_internalHeight - 1); + for (int x = 0; x < _internalWidth; x++) + { + float nx = x / (float)(_internalWidth - 1); + float wave = MathF.Sin(nx * 8f + timeSeconds * (0.3f + mid * 1.2f) + MathF.Sin(nx * 3f + timeSeconds)); + float layer = MathF.Exp(-MathF.Abs(ny - (0.35f + wave * 0.18f)) * (6f + high * 12f)); + float layer2 = MathF.Exp(-MathF.Abs(ny - (0.55f + MathF.Sin(nx * 5f - timeSeconds * 0.4f) * 0.12f)) * 9f); + float a = Clamp01(layer + layer2 * 0.7f); + float r = a * 0.15f; + float g = a * (0.6f + high * 0.5f); + float b = a * (0.45f + mid * 0.5f); + _pixelBuffer[y * _internalWidth + x] = new Color(r, g, b, 1f); + } + } + } + + private void RenderVortexPortal(AudioFeatures features, float timeSeconds) + { + float bass = Clamp01(features.Bass * 0.012f); + float aspect = _internalWidth / (float)_internalHeight; + for (int y = 0; y < _internalHeight; y++) + { + float py = (y / (float)_internalHeight) * 2f - 1f; + for (int x = 0; x < _internalWidth; x++) + { + float px = ((x / (float)_internalWidth) * 2f - 1f) * aspect; + float r = MathF.Sqrt(px * px + py * py) + 1e-5f; + float a = MathF.Atan2(py, px) + timeSeconds * (0.5f + bass * 2f); + float spiral = MathF.Sin(14f * r - a * 5f - timeSeconds * 3f); + float glow = MathF.Exp(-r * (2.5f - bass)); + float v = Clamp01((spiral * 0.5f + 0.5f) * glow * 2f); + _pixelBuffer[y * _internalWidth + x] = new Color(v * 0.7f, v * 0.4f, v, 1f); + } + } + } + + private void RenderLaserGrid(AudioFeatures features, float timeSeconds) + { + float beat = Clamp01(features.Beat); + float bass = Clamp01(features.Bass * 0.012f); + for (int y = 0; y < _internalHeight; y++) + { + float fy = y / (float)(_internalHeight - 1); + for (int x = 0; x < _internalWidth; x++) + { + float fx = x / (float)(_internalWidth - 1); + float perspective = 1f / (0.2f + fy * (1.8f - bass * 0.6f)); + float gx = MathF.Abs(((fx * perspective * 20f + timeSeconds) % 1f) - 0.5f); + float gy = MathF.Abs(((fy * perspective * 20f) % 1f) - 0.5f); + float line = Clamp01(0.08f / (gx + 0.02f) + 0.08f / (gy + 0.02f)); + float sweep = MathF.Exp(-MathF.Abs(fy - ((timeSeconds * 0.25f + beat * 0.2f) % 1f)) * 60f); + float v = Clamp01(line * 0.6f + sweep); + _pixelBuffer[y * _internalWidth + x] = new Color(v * 0.1f, v * 0.9f, v, 1f); + } + } + } + + private void RenderRainRipple(AudioFeatures features, float timeSeconds) + { + float level = Clamp01(features.Level * 3f); + float density = 70f + level * 220f; + for (int y = 0; y < _internalHeight; y++) + { + for (int x = 0; x < _internalWidth; x++) + { + _pixelBuffer[y * _internalWidth + x] = new Color(0.02f, 0.04f, 0.08f, 1f); + } + } + + int drops = (int)density; + for (int i = 0; i < drops; i++) + { + float seed = i * 13.37f; + float x = Frac(seed * 0.731f); + float phase = Frac(seed * 0.379f + timeSeconds * (0.4f + level * 1.2f)); + float y = phase; + int px = (int)(x * _internalWidth); + int py = (int)(y * _internalHeight); + DrawRect(px, py, 1, 6, new Vector3(0.6f, 0.8f, 1f), 0.7f); + + if (py > _internalHeight * 0.8f) + { + float rr = (phase - 0.8f) * 80f; + DrawRing(px, (int)(_internalHeight * 0.82f), rr, new Vector3(0.7f, 0.9f, 1f), 0.35f); + } + } + } + + private void RenderInkSmoke(AudioFeatures features, float timeSeconds) + { + float bass = Clamp01(features.Bass * 0.012f); + float high = Clamp01(features.High * 0.003f); + for (int y = 0; y < _internalHeight; y++) + { + float ny = y / (float)(_internalHeight - 1); + for (int x = 0; x < _internalWidth; x++) + { + float nx = (x / (float)(_internalWidth - 1)) * 2f - 1f; + float basePlume = MathF.Exp(-MathF.Abs(nx) * (2.5f - bass)); + float turbulence = MathF.Sin(nx * 12f + ny * 9f - timeSeconds * (1.5f + high * 5f)) + + MathF.Sin(nx * 21f - ny * 14f + timeSeconds * 2.2f); + turbulence = turbulence * 0.25f + 0.5f; + float rise = Clamp01((1f - ny) * (0.7f + bass * 0.7f)); + float d = Clamp01(basePlume * rise * turbulence); + _pixelBuffer[y * _internalWidth + x] = new Color(d * 0.18f, d * 0.2f, d * 0.24f, 1f); + } + } + } + + private void RenderMirrorMosaic(AudioFeatures features, float timeSeconds) + { + int tiles = 3 + (int)(Clamp01(features.Beat + features.Level * 2f) * 5f); + float t = timeSeconds * 0.6f; + for (int y = 0; y < _internalHeight; y++) + { + float v = y / (float)_internalHeight; + for (int x = 0; x < _internalWidth; x++) + { + float u = x / (float)_internalWidth; + float mu = Frac(u * tiles); + float mv = Frac(v * tiles); + if (((int)(u * tiles) & 1) == 1) mu = 1f - mu; + if (((int)(v * tiles) & 1) == 1) mv = 1f - mv; + float pattern = MathF.Sin((mu * 10f + t) * MathF.Tau) * MathF.Cos((mv * 10f - t) * MathF.Tau); + float p = pattern * 0.5f + 0.5f; + _pixelBuffer[y * _internalWidth + x] = new Color( + Clamp01(0.2f + p * 0.9f), + Clamp01(0.2f + (1f - p) * 0.8f), + Clamp01(0.35f + p * 0.7f), + 1f); + } + } + } + + private void RenderWaveformRibbon(AudioFeatures features, float timeSeconds) + { + for (int i = 0; i < _pixelBuffer.Length; i++) + { + _pixelBuffer[i] = new Color(0.01f, 0.01f, 0.03f, 1f); + } + + if (features.Spectrum is null || features.Spectrum.Length == 0) + { + return; + } + + int points = _internalWidth; + float beat = Clamp01(features.Beat); + for (int x = 0; x < points; x++) + { + float tx = x / (float)(points - 1); + float idx = tx * (features.Spectrum.Length - 1); + int i0 = (int)idx; + int i1 = Math.Min(features.Spectrum.Length - 1, i0 + 1); + float f = idx - i0; + float amp = (features.Spectrum[i0] + (features.Spectrum[i1] - features.Spectrum[i0]) * f) * 0.18f; + amp = Clamp01(amp); + float center = 0.5f + MathF.Sin(tx * 8f + timeSeconds * 0.9f) * 0.08f; + float yNorm = center - amp * (0.35f + beat * 0.2f); + int y = (int)(yNorm * _internalHeight); + int thickness = 2 + (int)(amp * 9f); + for (int k = -thickness; k <= thickness; k++) + { + int yy = y + k; + if (yy < 0 || yy >= _internalHeight) + { + continue; + } + + float w = 1f - MathF.Abs(k) / (float)(thickness + 1); + Vector3 c = new(0.2f + amp * 1.0f, 0.3f + w * 0.7f, 0.7f + beat * 0.3f); + BlendPixel(x, yy, c, w * 0.8f); + } + } + } + + private void RenderGrainBars(AudioFeatures features, float timeSeconds) + { + float bass = Clamp01(features.Bass * 0.012f); + float mid = Clamp01(features.Mid * 0.0035f); + float high = Clamp01(features.High * 0.0035f); + float beat = Clamp01(features.Beat); + float level = Clamp01(features.Level * 3f); + + for (int y = 0; y < _internalHeight; y++) + { + float t = y / (float)(_internalHeight - 1); + Color bg = new(0.015f, 0.02f + 0.04f * (1f - t), 0.03f + 0.07f * (1f - t), 1f); + int row = y * _internalWidth; + for (int x = 0; x < _internalWidth; x++) + { + _pixelBuffer[row + x] = bg; + } + } + + int totalBars = Math.Max(120, _internalWidth / 2); + float bandWidth = _internalWidth / 3f; + float maxHeight = _internalHeight * (0.22f + 0.62f * (0.35f + level * 0.65f)); + + for (int i = 0; i < totalBars; i++) + { + float xNorm = i / (float)(totalBars - 1); + int x = (int)(xNorm * (_internalWidth - 1)); + int region = x < bandWidth ? 0 : (x < bandWidth * 2f ? 1 : 2); + + float band = region switch + { + 0 => bass, + 1 => mid, + _ => high + }; + + float grain = Hash01(i, (int)(timeSeconds * 60f), region) * 2f - 1f; + float wobble = MathF.Sin(timeSeconds * (6.0f + region * 2.0f) + i * 0.21f) * 0.22f; + float spectrum = GetSpectrumSample(features, i * 0.43f + region * 7f) * 1.8f; + + float amp = Clamp01(band * 0.8f + spectrum * 0.45f + beat * 0.3f + grain * 0.13f + wobble * 0.1f); + int h = Math.Max(2, (int)(amp * maxHeight)); + + Vector3 color = region switch + { + 0 => new Vector3(1f, 0.35f + amp * 0.4f, 0.2f), + 1 => new Vector3(0.2f, 0.9f, 0.35f + amp * 0.5f), + _ => new Vector3(0.25f + amp * 0.4f, 0.5f, 1f) + }; + + int barW = 1; + int y0 = _internalHeight - h; + DrawRect(x, y0, barW, h, color, 0.95f); + + if (Hash01(i, region, (int)(timeSeconds * 100f)) > 0.68f) + { + int gritCount = 2 + (int)(amp * 8f); + for (int g = 0; g < gritCount; g++) + { + int gy = _internalHeight - 1 - (int)(Hash01(g, i, region) * h); + DrawRect(x, gy, 1, 1, Vector3.One, 0.3f + amp * 0.5f); + } + } + } + } + private void SpawnFireworkRocket(float intensity) { float x = 0.1f + (float)_rng.NextDouble() * 0.8f; @@ -782,6 +1222,35 @@ public sealed class FractalRenderer : IDisposable } } + private void DrawRing(int cx, int cy, float radius, Vector3 color, float alpha) + { + int r = Math.Max(1, (int)radius); + int minX = Math.Max(0, cx - r - 1); + int maxX = Math.Min(_internalWidth - 1, cx + r + 1); + int minY = Math.Max(0, cy - r - 1); + int maxY = Math.Min(_internalHeight - 1, cy + r + 1); + float r0 = MathF.Max(0f, radius - 1.25f); + float r1 = radius + 1.25f; + float r0s = r0 * r0; + float r1s = r1 * r1; + + for (int y = minY; y <= maxY; y++) + { + float dy = y - cy; + for (int x = minX; x <= maxX; x++) + { + float dx = x - cx; + float d2 = dx * dx + dy * dy; + if (d2 < r0s || d2 > r1s) + { + continue; + } + + BlendPixel(x, y, color, alpha); + } + } + } + private void BlendPixel(int x, int y, Vector3 color, float alpha) { int idx = y * _internalWidth + x;