Files
SoundFlix/FractalVisualizer/Rendering/FractalRenderer.cs
T
Michal Vrba 6a1bdfb935 Add real-time note detection and NOTEDROP visualization
Implemented musical note detection in AudioAnalyzer, exposing dominant note and strength in AudioFeatures. Updated HUD to display detected note. Added "NOTEDROP" fractal preset that animates detected notes as colored glyphs. Extended bitmap font to support sharps. Increased fractal preset count and adjusted HUD layout.
2026-02-13 20:43:29 +01:00

1649 lines
59 KiB
C#

using System;
using System.Collections.Generic;
using FractalVisualizer.Models;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace FractalVisualizer.Rendering;
public sealed class FractalRenderer : IDisposable
{
private readonly GraphicsDevice _graphicsDevice;
private readonly SpriteBatch _spriteBatch;
private int _internalWidth;
private int _internalHeight;
private Texture2D _fractalTexture;
private Color[] _pixelBuffer;
private Color[] _postBuffer;
private Color[] _previousFrame;
private readonly List<NoteParticle> _notes = [];
private readonly List<DetectedNoteParticle> _detectedNotes = [];
private readonly List<FireworkRocket> _fireworkRockets = [];
private readonly List<FireworkSpark> _fireworkSparks = [];
private readonly Random _rng = new();
private static readonly string[] NoteNames =
[
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
];
private static readonly Dictionary<char, string[]> NoteGlyphs = new()
{
['A'] = ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
['B'] = ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
['C'] = ["01111", "10000", "10000", "10000", "10000", "10000", "01111"],
['D'] = ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
['E'] = ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
['F'] = ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
['G'] = ["01111", "10000", "10000", "10111", "10001", "10001", "01111"],
['#'] = ["01010", "11111", "01010", "01010", "11111", "01010", "00000"]
};
private float _noteSpawnAccumulator;
private float _detectedNoteAccumulator;
private float _fireworkSpawnAccumulator;
private float _runtimePerformanceScale = 1f;
private RenderQuality _quality = RenderQuality.Medium;
private int _lastViewportWidth;
private int _lastViewportHeight;
private bool _disposed;
public FractalRenderer(GraphicsDevice graphicsDevice, int internalWidth = 320, int internalHeight = 180)
{
_graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
_spriteBatch = new SpriteBatch(_graphicsDevice);
_internalWidth = Math.Max(32, internalWidth);
_internalHeight = Math.Max(18, internalHeight);
_fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color);
_pixelBuffer = new Color[_internalWidth * _internalHeight];
_postBuffer = new Color[_internalWidth * _internalHeight];
_previousFrame = new Color[_internalWidth * _internalHeight];
_lastViewportWidth = _graphicsDevice.Viewport.Width;
_lastViewportHeight = _graphicsDevice.Viewport.Height;
}
public RenderQuality Quality => _quality;
public void SetQuality(RenderQuality quality)
{
if (_quality == quality)
{
return;
}
_quality = quality;
ResizeForQuality();
}
public void UpdatePerformanceHint(int currentFps, int targetFps)
{
if (targetFps <= 0)
{
_runtimePerformanceScale = 1f;
return;
}
float ratio = currentFps / (float)targetFps;
_runtimePerformanceScale = Math.Clamp(ratio, 0.55f, 1.15f);
}
private void EnsureRenderSurface()
{
Viewport viewport = _graphicsDevice.Viewport;
if (viewport.Width == _lastViewportWidth && viewport.Height == _lastViewportHeight)
{
return;
}
_lastViewportWidth = viewport.Width;
_lastViewportHeight = viewport.Height;
ResizeForQuality();
}
private void ResizeForQuality()
{
Viewport viewport = _graphicsDevice.Viewport;
float scale = _quality switch
{
RenderQuality.Low => 0.35f,
RenderQuality.Medium => 0.50f,
RenderQuality.High => 0.75f,
_ => 0.50f
};
int width = Math.Max(64, (int)(viewport.Width * scale));
int height = Math.Max(36, (int)(viewport.Height * scale));
ResizeBuffers(width, height);
}
private void ResizeBuffers(int width, int height)
{
if (_internalWidth == width && _internalHeight == height)
{
return;
}
_fractalTexture.Dispose();
_internalWidth = width;
_internalHeight = height;
_fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color);
_pixelBuffer = new Color[_internalWidth * _internalHeight];
_postBuffer = new Color[_internalWidth * _internalHeight];
_previousFrame = new Color[_internalWidth * _internalHeight];
_notes.Clear();
_detectedNotes.Clear();
_fireworkRockets.Clear();
_fireworkSparks.Clear();
_noteSpawnAccumulator = 0f;
_detectedNoteAccumulator = 0f;
_fireworkSpawnAccumulator = 0f;
}
private int ScaleCount(int baseCount, int minCount = 1)
{
float qualityScale = _quality switch
{
RenderQuality.Low => 0.70f,
RenderQuality.Medium => 1.0f,
RenderQuality.High => 1.35f,
_ => 1.0f
};
int scaled = (int)MathF.Round(baseCount * qualityScale * _runtimePerformanceScale);
return Math.Max(minCount, scaled);
}
public void Draw(AudioFeatures features, float timeSeconds, int presetIndex)
{
ThrowIfDisposed();
EnsureRenderSurface();
switch (presetIndex)
{
case 0:
RenderJulia(features, timeSeconds);
break;
case 1:
RenderMandelbulbStyle(features, timeSeconds);
break;
case 2:
RenderKaleidoscopeTunnel(features, timeSeconds);
break;
case 3:
RenderFlame(features, timeSeconds);
break;
case 4:
RenderPlasma(features, timeSeconds);
break;
case 5:
RenderNebula(features, timeSeconds);
break;
case 6:
RenderFlameGrill(features, timeSeconds);
break;
case 7:
RenderFloatingNotes(features, timeSeconds);
break;
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;
case 20:
RenderDetectedNotes(features, timeSeconds);
break;
default:
RenderJulia(features, timeSeconds);
break;
}
ApplyFeedbackAndCrt(features, timeSeconds);
_fractalTexture.SetData(_postBuffer);
Viewport viewport = _graphicsDevice.Viewport;
Rectangle destination = new(0, 0, viewport.Width, viewport.Height);
_spriteBatch.Begin(samplerState: SamplerState.LinearClamp, blendState: BlendState.Opaque);
_spriteBatch.Draw(_fractalTexture, destination, Color.White);
_spriteBatch.End();
}
private void ApplyFeedbackAndCrt(AudioFeatures features, float timeSeconds)
{
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) + (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++)
{
float ny = (y / (_internalHeight - 1f)) * 2f - 1f;
float scanline = 0.90f + (0.10f * MathF.Sin(((y * 0.42f) + (timeSeconds * 22f)) * MathF.PI));
for (int x = 0; x < _internalWidth; x++)
{
int index = (y * _internalWidth) + x;
Vector3 raw = _pixelBuffer[index].ToVector3();
Vector3 previous = _previousFrame[index].ToVector3();
Vector3 trailed = Vector3.Lerp(raw, previous, trailStrength);
int xr = Math.Min(_internalWidth - 1, x + shiftPixels);
int xb = Math.Max(0, x - shiftPixels);
int rightIndex = (y * _internalWidth) + xr;
int leftIndex = (y * _internalWidth) + xb;
float r = _pixelBuffer[rightIndex].R / 255f;
float g = trailed.Y;
float b = _pixelBuffer[leftIndex].B / 255f;
Vector3 crt = new(r, g, b);
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);
crt.Z = Clamp01(crt.Z);
Color finalColor = new(crt.X, crt.Y, crt.Z, 1f);
_postBuffer[index] = finalColor;
_previousFrame[index] = finalColor;
}
}
}
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);
float mid = Clamp01(features.Mid * 0.003f);
float high = Clamp01(features.High * 0.0025f);
float beat = Clamp01(features.Beat);
float power = 6f + (bass * 4f);
float detail = 28f + (high * 22f);
float rotation = timeSeconds * (0.25f + mid * 0.8f);
float aspect = _internalWidth / (float)_internalHeight;
float cosR = MathF.Cos(rotation);
float sinR = MathF.Sin(rotation);
for (int y = 0; y < _internalHeight; y++)
{
float sy = (y / (float)_internalHeight) * 2f - 1f;
for (int x = 0; x < _internalWidth; x++)
{
float sx = ((x / (float)_internalWidth) * 2f - 1f) * aspect;
float rayX = sx;
float rayY = sy;
float rayZ = 1.65f - beat * 0.25f;
float march = 0f;
float glow = 0f;
float minDist = 10f;
int steps = ScaleCount((int)detail, 14);
for (int step = 0; step < steps; step++)
{
float px = rayX * march;
float py = rayY * march;
float pz = rayZ - march;
float rpx = (px * cosR) - (pz * sinR);
float rpz = (px * sinR) + (pz * cosR);
float d = MandelbulbDistance(rpx, py, rpz, power);
minDist = MathF.Min(minDist, d);
glow += MathF.Exp(-20f * d) * (0.015f + high * 0.03f);
march += d * 0.95f;
if (d < 0.002f || march > 3.2f)
{
break;
}
}
float hit = Clamp01(1f - (minDist * 110f));
float fog = Clamp01(1f - (march / 3.2f));
float pulse = beat * 0.45f;
float r = Clamp01(hit * (0.35f + bass * 1.2f + pulse) + glow * 1.3f);
float g = Clamp01(hit * (0.22f + mid * 1.1f) + glow * 0.6f);
float b = Clamp01(hit * (0.45f + high * 1.5f) + glow * 1.5f + fog * 0.08f);
_pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f);
}
}
}
private void RenderKaleidoscopeTunnel(AudioFeatures features, float timeSeconds)
{
float bass = Clamp01(features.Bass * 0.01f);
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);
float aspect = _internalWidth / (float)_internalHeight;
float tunnelSpeed = 0.45f + bass * 1.35f;
float spin = timeSeconds * (0.25f + mid * 1.1f);
float slices = 6f + MathF.Floor(high * 8f);
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 radius = MathF.Sqrt((px * px) + (py * py)) + 1e-5f;
float angle = MathF.Atan2(py, px) + spin;
float wedge = MathF.PI / slices;
angle = MathF.Abs((angle % (2f * wedge)) - wedge);
float spectrumDrive = GetSpectrumSample(features, radius * 8f + beat * 10f);
float distortion = spectrumDrive * (0.55f + high * 0.7f);
float tunnel = MathF.Sin((1f / radius) * 9f - timeSeconds * tunnelSpeed * 7f + distortion * 8f);
float rings = MathF.Sin(radius * 35f - timeSeconds * (2f + mid * 4f));
float edge = MathF.Exp(-10f * MathF.Abs(angle - wedge * 0.5f));
float pulse = beat * 0.6f;
float brightness = Clamp01((tunnel * 0.5f + 0.5f) * (0.4f + level) + edge * 0.5f + pulse * 0.3f);
float hueShift = (timeSeconds * 0.15f) + (beat * 0.25f);
float r = Clamp01(brightness * (0.5f + 0.5f * MathF.Sin(hueShift + 0.0f)) + rings * 0.08f + bass * 0.25f);
float g = Clamp01(brightness * (0.5f + 0.5f * MathF.Sin(hueShift + 2.09f)) + rings * 0.06f + mid * 0.25f);
float b = Clamp01(brightness * (0.5f + 0.5f * MathF.Sin(hueShift + 4.18f)) + rings * 0.10f + high * 0.35f);
_pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f);
}
}
}
private float GetSpectrumSample(AudioFeatures features, float virtualIndex)
{
if (features.Spectrum is null || features.Spectrum.Length == 0)
{
return 0f;
}
int i0 = ((int)virtualIndex) % features.Spectrum.Length;
if (i0 < 0)
{
i0 += features.Spectrum.Length;
}
int i1 = (i0 + 1) % features.Spectrum.Length;
float t = virtualIndex - MathF.Floor(virtualIndex);
float sample = features.Spectrum[i0] + ((features.Spectrum[i1] - features.Spectrum[i0]) * t);
return Clamp01(sample * 0.06f);
}
private static float MandelbulbDistance(float x, float y, float z, float power)
{
float zx = x;
float zy = y;
float zz = z;
float dr = 1f;
float r = 0f;
for (int i = 0; i < 7; i++)
{
r = MathF.Sqrt((zx * zx) + (zy * zy) + (zz * zz));
if (r > 2f)
{
break;
}
float theta = MathF.Acos(zz / (r + 1e-6f));
float phi = MathF.Atan2(zy, zx);
float zr = MathF.Pow(r, power);
dr = MathF.Pow(r, power - 1f) * power * dr + 1f;
float sinTheta = MathF.Sin(theta * power);
zx = zr * sinTheta * MathF.Cos(phi * power) + x;
zy = zr * sinTheta * MathF.Sin(phi * power) + y;
zz = zr * MathF.Cos(theta * power) + z;
}
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);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float level = Clamp01(features.Level * 3f);
float aspect = _internalWidth / (float)_internalHeight;
float flowSpeed = 0.65f + (bass * 1.8f);
float turbulence = 1.2f + (high * 3.0f);
float pulse = 0.2f + (beat * 0.8f);
for (int y = 0; y < _internalHeight; y++)
{
float y01 = y / (float)(_internalHeight - 1);
float fy = 1f - y01;
for (int x = 0; x < _internalWidth; x++)
{
float x01 = x / (float)(_internalWidth - 1);
float fx = (x01 * 2f - 1f) * aspect;
float t = timeSeconds * flowSpeed;
float n1 = MathF.Sin((fx * 6.0f) + (fy * 7.5f) - t * 5.0f);
float n2 = MathF.Sin((fx * 12.0f - t * 2.1f) + MathF.Cos(fy * 10.0f + t * 3.3f));
float n3 = MathF.Sin((fx * 22.0f + t * 1.4f) + MathF.Sin(fy * 18.0f - t * 2.7f));
float noise = ((n1 * 0.55f) + (n2 * 0.30f) + (n3 * 0.15f)) * turbulence;
noise = noise * 0.5f + 0.5f;
float cone = 1f - MathF.Abs(fx) * (0.55f + fy * 0.65f);
cone = Clamp01(cone);
float baseIntensity = MathF.Pow(fy, 1.8f);
float flame = Clamp01(baseIntensity * cone * (0.6f + noise * 0.9f + pulse * 0.25f));
flame *= 0.65f + level * 0.75f;
float ember = MathF.Sin((fx * 45f) + (fy * 14f) - t * 18f);
float sparks = ember > 0.985f ? 0.35f + beat * 0.45f : 0f;
float r = Clamp01(flame * 2.1f + sparks);
float g = Clamp01(MathF.Pow(flame, 1.35f) * (0.95f + high * 0.45f) + sparks * 0.7f);
float b = Clamp01(MathF.Pow(flame, 3.2f) * (0.25f + beat * 0.25f));
_pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f);
}
}
}
private void RenderPlasma(AudioFeatures features, float timeSeconds)
{
float bass = Clamp01(features.Bass * 0.01f);
float mid = Clamp01(features.Mid * 0.003f);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float level = Clamp01(features.Level * 3f);
float aspect = _internalWidth / (float)_internalHeight;
float speed = 0.8f + bass * 1.6f;
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 t = timeSeconds * speed;
float v1 = MathF.Sin((px * 4.5f) + t * 1.9f);
float v2 = MathF.Sin((py * 5.0f) - t * 1.3f);
float v3 = MathF.Sin((px + py) * 4.0f + t * (2.2f + mid * 2f));
float v4 = MathF.Sin(MathF.Sqrt(px * px + py * py) * 10f - t * (3f + high * 3f));
float plasma = (v1 + v2 + v3 + v4) * 0.25f;
float p = plasma * 0.5f + 0.5f;
float pulse = beat * 0.35f;
float spec = GetSpectrumSample(features, (x * 0.08f) + (y * 0.03f));
float r = Clamp01((0.35f + 0.65f * MathF.Sin((p + pulse) * 6.28f)) * (0.6f + level * 0.6f));
float g = Clamp01((0.35f + 0.65f * MathF.Sin((p + 0.33f) * 6.28f + spec * 2f)) * (0.55f + mid * 0.8f));
float b = Clamp01((0.35f + 0.65f * MathF.Sin((p + 0.66f) * 6.28f + spec * 3f)) * (0.55f + high * 0.9f + pulse));
_pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f);
}
}
}
private void RenderNebula(AudioFeatures features, float timeSeconds)
{
float bass = Clamp01(features.Bass * 0.01f);
float mid = Clamp01(features.Mid * 0.003f);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float level = Clamp01(features.Level * 3f);
float aspect = _internalWidth / (float)_internalHeight;
float zoom = 1.6f - bass * 0.5f;
float driftX = MathF.Sin(timeSeconds * (0.16f + mid * 0.4f)) * 0.45f;
float driftY = MathF.Cos(timeSeconds * (0.12f + high * 0.5f)) * 0.45f;
for (int y = 0; y < _internalHeight; y++)
{
float py = ((y / (float)_internalHeight) * 2f - 1f) * zoom;
for (int x = 0; x < _internalWidth; x++)
{
float px = (((x / (float)_internalWidth) * 2f - 1f) * aspect) * zoom;
float fx = px + driftX;
float fy = py + driftY;
float density = 0f;
float amp = 0.55f;
float freq = 1.1f;
for (int o = 0; o < 5; o++)
{
float n = MathF.Sin((fx * freq + timeSeconds * 0.15f) * 2.1f)
* MathF.Cos((fy * freq - timeSeconds * 0.19f) * 2.3f);
density += (n * 0.5f + 0.5f) * amp;
freq *= 1.9f;
amp *= 0.55f;
}
float rad = MathF.Sqrt(fx * fx + fy * fy);
float core = MathF.Exp(-rad * (1.6f + bass * 1.2f));
float stars = GetSpectrumSample(features, rad * 24f + timeSeconds * 8f);
float pulse = beat * 0.45f;
float cloud = Clamp01(density * (0.6f + level * 0.5f));
float r = Clamp01(cloud * (0.25f + mid * 0.8f) + core * (0.8f + pulse));
float g = Clamp01(cloud * (0.35f + high * 0.9f) + core * 0.55f + stars * 0.35f);
float b = Clamp01(cloud * (0.55f + bass * 0.8f) + core * 0.75f + stars * 0.5f + pulse * 0.2f);
_pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f);
}
}
}
private void RenderFlameGrill(AudioFeatures features, float timeSeconds)
{
float level = Clamp01(features.Level * 3f);
float bass = Clamp01(features.Bass * 0.012f);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float intensity = Clamp01(0.25f + level * 0.45f + bass * 0.35f + beat * 0.35f);
float flameHeight = 0.22f + intensity * 0.68f;
float flickerSpeed = 2.2f + bass * 4.0f;
int grillTop = (int)(_internalHeight * 0.86f);
float barSpacing = 14f;
float barThickness = 3.5f;
for (int y = 0; y < _internalHeight; y++)
{
float y01 = y / (float)(_internalHeight - 1);
float fromBottom = 1f - y01;
for (int x = 0; x < _internalWidth; x++)
{
int idx = (y * _internalWidth) + x;
float x01 = x / (float)(_internalWidth - 1);
float wave1 = MathF.Sin((x01 * 18f) + timeSeconds * flickerSpeed);
float wave2 = MathF.Sin((x01 * 36f) - timeSeconds * (flickerSpeed * 0.65f));
float wave3 = MathF.Sin((x01 * 72f) + timeSeconds * (flickerSpeed * 1.35f));
float turbulence = (wave1 * 0.45f + wave2 * 0.35f + wave3 * 0.20f) * (0.22f + high * 0.55f);
float localHeight = flameHeight + turbulence;
float flameMask = Clamp01((fromBottom - 0.02f) / MathF.Max(0.05f, localHeight));
float taper = Clamp01(1f - MathF.Pow(fromBottom / MathF.Max(0.05f, localHeight), 1.35f));
float flameCore = flameMask * taper;
float tongues = MathF.Sin((x01 * 110f) + timeSeconds * (6f + high * 8f) - fromBottom * 22f);
float tongueMask = Clamp01(tongues * 0.5f + 0.5f);
float flame = flameCore * (0.72f + tongueMask * 0.45f);
flame *= 0.65f + intensity * 0.7f;
float emberNoise = MathF.Sin((x01 * 180f) + (fromBottom * 90f) - timeSeconds * 18f);
float sparks = emberNoise > 0.992f && fromBottom < localHeight ? 0.55f + beat * 0.35f : 0f;
float r = Clamp01(flame * 2.4f + sparks);
float g = Clamp01(MathF.Pow(flame, 1.4f) * (1.05f + high * 0.35f) + sparks * 0.55f);
float b = Clamp01(MathF.Pow(flame, 3.1f) * (0.22f + beat * 0.18f));
if (y >= grillTop)
{
float line = MathF.Abs(((x % barSpacing) - barSpacing * 0.5f));
float barMask = Clamp01(1f - (line / barThickness));
float hot = 0.25f + intensity * 0.55f;
float metal = 0.08f;
float grillR = Clamp01(metal + barMask * hot);
float grillG = Clamp01(metal * 0.7f + barMask * hot * 0.45f);
float grillB = Clamp01(metal * 0.6f + barMask * hot * 0.18f);
r = MathF.Max(r * 0.45f, grillR);
g = MathF.Max(g * 0.45f, grillG);
b = MathF.Max(b * 0.45f, grillB);
}
_pixelBuffer[idx] = new Color(r, g, b, 1f);
}
}
}
private void RenderFloatingNotes(AudioFeatures features, float timeSeconds)
{
float level = Clamp01(features.Level * 3f);
float bass = Clamp01(features.Bass * 0.012f);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float intensity = Clamp01(level * 0.6f + bass * 0.25f + beat * 0.35f);
for (int y = 0; y < _internalHeight; y++)
{
float t = y / (float)(_internalHeight - 1);
float r = 0.03f + 0.08f * (1f - t);
float g = 0.02f + 0.05f * (1f - t);
float b = 0.08f + 0.20f * (1f - t);
Color bg = new(r, g, b, 1f);
int row = y * _internalWidth;
for (int x = 0; x < _internalWidth; x++)
{
_pixelBuffer[row + x] = bg;
}
}
float spawnRate = (0.08f + intensity * 1.8f + high * 0.7f) * _runtimePerformanceScale;
_noteSpawnAccumulator += spawnRate;
while (_noteSpawnAccumulator >= 1f)
{
SpawnNote(intensity);
_noteSpawnAccumulator -= 1f;
}
for (int i = _notes.Count - 1; i >= 0; i--)
{
NoteParticle note = _notes[i];
note.Y += note.Vy;
note.X += note.Vx + MathF.Sin(timeSeconds * 2.8f + note.Phase) * 0.0012f;
note.Life -= 0.012f + (1f - intensity) * 0.004f;
if (note.Life <= 0f || note.Y < -0.12f || note.X < -0.2f || note.X > 1.2f)
{
_notes.RemoveAt(i);
continue;
}
_notes[i] = note;
DrawNote(note);
}
}
private void RenderFireworks(AudioFeatures features, float timeSeconds)
{
float level = Clamp01(features.Level * 3f);
float bass = Clamp01(features.Bass * 0.012f);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float intensity = Clamp01(level * 0.55f + bass * 0.30f + beat * 0.45f);
for (int y = 0; y < _internalHeight; y++)
{
float t = y / (float)(_internalHeight - 1);
float r = 0.01f + 0.05f * (1f - t);
float g = 0.01f + 0.04f * (1f - t);
float b = 0.04f + 0.16f * (1f - t);
Color bg = new(r, g, b, 1f);
int row = y * _internalWidth;
for (int x = 0; x < _internalWidth; x++)
{
_pixelBuffer[row + x] = bg;
}
}
float spawnRate = (0.02f + intensity * 0.45f + high * 0.15f) * _runtimePerformanceScale;
_fireworkSpawnAccumulator += spawnRate;
while (_fireworkSpawnAccumulator >= 1f)
{
SpawnFireworkRocket(intensity);
_fireworkSpawnAccumulator -= 1f;
}
for (int i = _fireworkRockets.Count - 1; i >= 0; i--)
{
FireworkRocket rocket = _fireworkRockets[i];
rocket.Y += rocket.Vy;
rocket.X += rocket.Vx;
rocket.Age += 0.016f;
DrawFilledCircle(
(int)(rocket.X * _internalWidth),
(int)(rocket.Y * _internalHeight),
1 + (int)(rocket.Size * 0.6f),
rocket.Color,
0.95f);
bool explode = rocket.Y <= rocket.TargetY || rocket.Age >= rocket.MaxAge;
if (explode)
{
ExplodeFirework(rocket, intensity);
_fireworkRockets.RemoveAt(i);
}
else
{
_fireworkRockets[i] = rocket;
}
}
for (int i = _fireworkSparks.Count - 1; i >= 0; i--)
{
FireworkSpark spark = _fireworkSparks[i];
spark.X += spark.Vx;
spark.Y += spark.Vy;
spark.Vy += 0.00022f;
spark.Vx *= 0.995f;
spark.Life -= 0.012f;
if (spark.Life <= 0f || spark.Y > 1.1f || spark.X < -0.2f || spark.X > 1.2f)
{
_fireworkSparks.RemoveAt(i);
continue;
}
_fireworkSparks[i] = spark;
int sx = (int)(spark.X * _internalWidth);
int sy = (int)(spark.Y * _internalHeight);
DrawFilledCircle(sx, sy, spark.Size, spark.Color, spark.Life);
}
}
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 = ScaleCount(2200, 600);
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 = ScaleCount((int)density, 20);
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 = ScaleCount(Math.Max(120, _internalWidth / 2), 60);
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 RenderDetectedNotes(AudioFeatures features, float timeSeconds)
{
for (int y = 0; y < _internalHeight; y++)
{
float t = y / (float)(_internalHeight - 1);
Color bg = new(0.02f, 0.02f + 0.04f * (1f - t), 0.05f + 0.10f * (1f - t), 1f);
int row = y * _internalWidth;
for (int x = 0; x < _internalWidth; x++)
{
_pixelBuffer[row + x] = bg;
}
}
float spawnRate = features.DetectedNoteStrength * (0.3f + Clamp01(features.Beat) * 1.3f);
_detectedNoteAccumulator += spawnRate * _runtimePerformanceScale;
while (_detectedNoteAccumulator >= 1f)
{
SpawnDetectedNote(features);
_detectedNoteAccumulator -= 1f;
}
for (int i = _detectedNotes.Count - 1; i >= 0; i--)
{
DetectedNoteParticle p = _detectedNotes[i];
p.X += p.Vx;
p.Y += p.Vy;
p.Vy -= 0.00002f;
p.Life -= 0.008f + (1f - features.DetectedNoteStrength) * 0.006f;
if (p.Life <= 0f || p.X < -0.2f || p.X > 1.2f || p.Y < -0.2f)
{
_detectedNotes.RemoveAt(i);
continue;
}
_detectedNotes[i] = p;
DrawDetectedNoteText(p);
}
}
private void SpawnDetectedNote(AudioFeatures features)
{
if (features.DetectedNoteIndex < 0 || features.DetectedNoteIndex > 11)
{
return;
}
if (_detectedNotes.Count >= ScaleCount(96, 20))
{
return;
}
string text = NoteNames[features.DetectedNoteIndex];
float x = 0.1f + (float)_rng.NextDouble() * 0.8f;
float y = 0.25f + (float)_rng.NextDouble() * 0.65f;
float vx = ((float)_rng.NextDouble() - 0.5f) * 0.0022f;
float vy = -(0.0014f + (float)_rng.NextDouble() * 0.0025f);
float scale = 1.2f + (float)_rng.NextDouble() * (1.8f + features.DetectedNoteStrength * 1.6f);
float hue = features.DetectedNoteIndex / 12f;
Vector3 color = new(
0.5f + 0.5f * MathF.Sin(hue * MathF.Tau),
0.5f + 0.5f * MathF.Sin(hue * MathF.Tau + 2.09f),
0.5f + 0.5f * MathF.Sin(hue * MathF.Tau + 4.18f));
_detectedNotes.Add(new DetectedNoteParticle
{
X = x,
Y = y,
Vx = vx,
Vy = vy,
Life = 1f,
Scale = scale,
Text = text,
Color = color
});
}
private void DrawDetectedNoteText(DetectedNoteParticle p)
{
int px = (int)(p.X * _internalWidth);
int py = (int)(p.Y * _internalHeight);
int scale = Math.Max(1, (int)MathF.Round(p.Scale));
for (int i = 0; i < p.Text.Length; i++)
{
DrawGlyph(p.Text[i], px + i * (6 * scale), py, scale, p.Color, p.Life);
}
}
private void DrawGlyph(char c, int x, int y, int scale, Vector3 color, float alpha)
{
if (!NoteGlyphs.TryGetValue(c, out string[]? glyph))
{
return;
}
for (int row = 0; row < glyph.Length; row++)
{
string line = glyph[row];
for (int col = 0; col < line.Length; col++)
{
if (line[col] != '1')
{
continue;
}
DrawRect(x + col * scale, y + row * scale, scale, scale, color, alpha);
}
}
}
private void SpawnFireworkRocket(float intensity)
{
if (_fireworkRockets.Count >= ScaleCount(42, 12))
{
return;
}
float x = 0.1f + (float)_rng.NextDouble() * 0.8f;
float y = 1.02f;
float vy = -(0.0065f + (float)_rng.NextDouble() * (0.0025f + intensity * 0.0025f));
float vx = ((float)_rng.NextDouble() - 0.5f) * 0.0018f;
float targetY = 0.2f + (float)_rng.NextDouble() * 0.42f;
float maxAge = 0.7f + (float)_rng.NextDouble() * 0.45f;
Vector3 color = new(
0.55f + (float)_rng.NextDouble() * 0.45f,
0.45f + (float)_rng.NextDouble() * 0.55f,
0.45f + (float)_rng.NextDouble() * 0.55f);
_fireworkRockets.Add(new FireworkRocket
{
X = x,
Y = y,
Vx = vx,
Vy = vy,
TargetY = targetY,
Age = 0f,
MaxAge = maxAge,
Size = 1.4f + (float)_rng.NextDouble() * 1.4f,
Color = color
});
}
private void ExplodeFirework(FireworkRocket rocket, float intensity)
{
int count = ScaleCount(22 + (int)(intensity * 38f), 10);
float speedBase = 0.0018f + intensity * 0.0034f;
for (int i = 0; i < count; i++)
{
if (_fireworkSparks.Count >= ScaleCount(4500, 800))
{
break;
}
float a = (float)(i * (Math.PI * 2.0 / count));
float speed = speedBase + (float)_rng.NextDouble() * speedBase;
float vx = MathF.Cos(a) * speed;
float vy = MathF.Sin(a) * speed;
_fireworkSparks.Add(new FireworkSpark
{
X = rocket.X,
Y = rocket.Y,
Vx = vx,
Vy = vy,
Life = 0.65f + (float)_rng.NextDouble() * 0.35f,
Size = 1 + _rng.Next(0, 2),
Color = rocket.Color
});
}
}
private void SpawnNote(float intensity)
{
if (_notes.Count >= ScaleCount(320, 80))
{
return;
}
float x = 0.08f + (float)_rng.NextDouble() * 0.84f;
float y = 0.94f + (float)_rng.NextDouble() * 0.08f;
float vy = -(0.0025f + (float)_rng.NextDouble() * (0.0035f + intensity * 0.004f));
float vx = ((float)_rng.NextDouble() - 0.5f) * (0.0018f + intensity * 0.0015f);
float size = 0.75f + (float)_rng.NextDouble() * (1.6f + intensity * 1.2f);
bool flagged = _rng.NextDouble() > 0.42;
Vector3 color = new(
0.7f + (float)_rng.NextDouble() * 0.3f,
0.55f + (float)_rng.NextDouble() * 0.45f,
0.4f + (float)_rng.NextDouble() * 0.6f);
_notes.Add(new NoteParticle
{
X = x,
Y = y,
Vx = vx,
Vy = vy,
Life = 1f,
Size = size,
Phase = (float)_rng.NextDouble() * MathF.PI * 2f,
Color = color,
HasFlag = flagged
});
}
private void DrawNote(NoteParticle note)
{
int px = (int)(note.X * _internalWidth);
int py = (int)(note.Y * _internalHeight);
int s = Math.Max(1, (int)MathF.Round(note.Size));
float alpha = Clamp01(note.Life);
Vector3 color = note.Color;
int headX = px;
int headY = py;
DrawFilledCircle(headX, headY, s + 1, color, alpha * 0.75f);
DrawFilledCircle(headX, headY, s, color, alpha);
int stemHeight = 6 + s * 4;
DrawRect(headX + s, headY - stemHeight, 2, stemHeight, color, alpha);
if (note.HasFlag)
{
for (int i = 0; i < 6 + s; i++)
{
int fx = headX + s + 1 + i;
int fy = headY - stemHeight + (int)(MathF.Sin(i * 0.55f) * (2f + s * 0.25f));
DrawRect(fx, fy, 2, 2, color, alpha * 0.95f);
}
}
}
private void DrawFilledCircle(int cx, int cy, int radius, Vector3 color, float alpha)
{
int r2 = radius * radius;
for (int oy = -radius; oy <= radius; oy++)
{
int yy = cy + oy;
if (yy < 0 || yy >= _internalHeight)
{
continue;
}
int oy2 = oy * oy;
for (int ox = -radius; ox <= radius; ox++)
{
if (ox * ox + oy2 > r2)
{
continue;
}
int xx = cx + ox;
if (xx < 0 || xx >= _internalWidth)
{
continue;
}
BlendPixel(xx, yy, color, alpha);
}
}
}
private void DrawRect(int x, int y, int width, int height, Vector3 color, float alpha)
{
for (int yy = y; yy < y + height; yy++)
{
if (yy < 0 || yy >= _internalHeight)
{
continue;
}
for (int xx = x; xx < x + width; xx++)
{
if (xx < 0 || xx >= _internalWidth)
{
continue;
}
BlendPixel(xx, yy, color, alpha);
}
}
}
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;
Vector3 baseColor = _pixelBuffer[idx].ToVector3();
Vector3 mixed = Vector3.Lerp(baseColor, color, Clamp01(alpha));
_pixelBuffer[idx] = new Color(Clamp01(mixed.X), Clamp01(mixed.Y), Clamp01(mixed.Z), 1f);
}
private void RenderJulia(AudioFeatures features, float timeSeconds)
{
float level = Clamp01(features.Level * 3f);
float bass = Clamp01(features.Bass * 0.015f);
float mid = Clamp01(features.Mid * 0.004f);
float high = Clamp01(features.High * 0.003f);
float beat = Clamp01(features.Beat);
float zoom = 1.1f - (bass * 0.45f) - (beat * 0.15f);
if (zoom < 0.45f)
{
zoom = 0.45f;
}
float cRe = -0.8f + (mid * 1.6f) + (MathF.Sin(timeSeconds * 0.31f) * 0.06f);
float cIm = 0.156f + ((high - 0.5f) * 1.0f) + (MathF.Cos(timeSeconds * 0.23f) * 0.04f);
int maxIterations = 42 + (int)(level * 54f);
float aspect = _internalWidth / (float)_internalHeight;
for (int y = 0; y < _internalHeight; y++)
{
float ny = ((y / (float)_internalHeight) * 2f - 1f) * zoom;
for (int x = 0; x < _internalWidth; x++)
{
float nx = (((x / (float)_internalWidth) * 2f - 1f) * aspect) * zoom;
float zr = nx;
float zi = ny;
int iter = 0;
while (iter < maxIterations)
{
float zr2 = zr * zr;
float zi2 = zi * zi;
if (zr2 + zi2 > 4f)
{
break;
}
zi = (2f * zr * zi) + cIm;
zr = zr2 - zi2 + cRe;
iter++;
}
float t = iter / (float)maxIterations;
float pulse = beat * 0.35f;
float r = MathF.Pow(t, 0.65f) * (0.35f + high * 1.2f + pulse);
float g = MathF.Pow(t, 1.2f) * (0.28f + mid * 1.1f);
float b = MathF.Pow(t, 0.85f) * (0.45f + bass * 1.5f + pulse);
_pixelBuffer[(y * _internalWidth) + x] = new Color(
Clamp01(r),
Clamp01(g),
Clamp01(b),
1f);
}
}
}
private static float Clamp01(float value)
{
if (value < 0f)
{
return 0f;
}
if (value > 1f)
{
return 1f;
}
return value;
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(FractalRenderer));
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_fractalTexture.Dispose();
_spriteBatch.Dispose();
_disposed = true;
}
private struct NoteParticle
{
public float X;
public float Y;
public float Vx;
public float Vy;
public float Life;
public float Size;
public float Phase;
public Vector3 Color;
public bool HasFlag;
}
private struct DetectedNoteParticle
{
public float X;
public float Y;
public float Vx;
public float Vy;
public float Life;
public float Scale;
public string Text;
public Vector3 Color;
}
private struct FireworkRocket
{
public float X;
public float Y;
public float Vx;
public float Vy;
public float TargetY;
public float Age;
public float MaxAge;
public float Size;
public Vector3 Color;
}
private struct FireworkSpark
{
public float X;
public float Y;
public float Vx;
public float Vy;
public float Life;
public int Size;
public Vector3 Color;
}
}