This commit is contained in:
Michal Vrba
2026-02-13 19:58:55 +01:00
parent 9b071ccf91
commit 5a9c290816
21 changed files with 2197 additions and 19 deletions
@@ -0,0 +1,924 @@
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 readonly int _internalWidth;
private readonly int _internalHeight;
private readonly Texture2D _fractalTexture;
private readonly Color[] _pixelBuffer;
private readonly Color[] _postBuffer;
private readonly Color[] _previousFrame;
private readonly List<NoteParticle> _notes = [];
private readonly List<FireworkRocket> _fireworkRockets = [];
private readonly List<FireworkSpark> _fireworkSparks = [];
private readonly Random _rng = new();
private float _noteSpawnAccumulator;
private float _fireworkSpawnAccumulator;
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 = internalWidth;
_internalHeight = 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];
}
public void Draw(AudioFeatures features, float timeSeconds, int presetIndex)
{
ThrowIfDisposed();
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;
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);
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);
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));
crt *= scanline;
crt *= 0.5f + (vignette * 0.5f);
crt += raw * glowBoost;
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 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 = (int)detail;
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 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;
_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;
_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 SpawnFireworkRocket(float intensity)
{
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 = 22 + (int)(intensity * 38f);
float speedBase = 0.0018f + intensity * 0.0034f;
for (int i = 0; i < count; i++)
{
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)
{
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 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 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;
}
}
@@ -0,0 +1 @@