animations/galaxy/galaxy.cs
|
using System;
using System.Collections.Generic; namespace AsciiFX.Animations { // A rotating spiral galaxy: stars scattered along trailing spiral arms around a glowing core, // each twinkling on its own rhythm. public class Galaxy : IAnimation { class Star { public double R, Angle, Bright, TwinkleSpeed, TwinklePhase; } const int Shades = 32; const string Charset = " .:-=+*#%@"; // faintest to brightest; index 0 is empty space static readonly int[] Blue = { 0x0a143c, 0x1e3ca0, 0x3c78e6, 0x78b4ff, 0xc8e6ff, 0xffffff }; static readonly int[] Magenta = { 0x280a3c, 0x6e1e96, 0xbe46dc, 0xf082f0, 0xffc8ff, 0xffffff }; readonly int[] palette; readonly int arms; readonly double speed, size, twist, noise, glow, twinkle; readonly Random rng = new Random(); int w, h; double radius; List<Star> stars; double[] light; public Galaxy(Options o) { palette = Colors.Gradient(Shades, o.Text("Palette", "blue") == "magenta" ? Magenta : Blue); arms = Math.Max(1, (int)o.Number("Arms", 2)); speed = o.Number("Speed", 20); size = o.Number("Size", 70); twist = o.Number("Twist", 0.45); noise = o.Number("Noise", 0.15); glow = o.Number("Glow", 0.45); twinkle = o.Number("Twinkle", 0.35); } public int[] Palette { get { return palette; } } public int FrameMs { get { return 33; } } public void Resize(int cols, int rows) { w = cols; h = rows; light = new double[w * h]; radius = Math.Max(2, size / 100.0 * Math.Min(h, w / Cell.Aspect) / 2); stars = MakeStars((int)(Math.PI * radius * radius * Cell.Aspect * 1.1)); } public void Frame(double t, char[] chars, int[] colors) { Array.Clear(light, 0, light.Length); double cx = (w - 1) / 2.0, cy = (h - 1) / 2.0; double rot = t * speed * Math.PI / 180; // Soft glow around the core if (glow > 0) { int reach = (int)Math.Ceiling(radius * 0.4); for (int y = Math.Max(0, (int)cy - reach); y <= Math.Min(h - 1, (int)cy + reach + 1); y++) for (int x = Math.Max(0, (int)(cx - reach * Cell.Aspect)); x <= Math.Min(w - 1, (int)(cx + reach * Cell.Aspect) + 1); x++) { double u = (x - cx) / Cell.Aspect / radius, v = (y - cy) / radius; double core = glow * 5 * Math.Exp(-(u * u + v * v) / 0.02); if (core > 0.02) light[y * w + x] += core; } } // Every star turns with the galaxy and twinkles on its own rhythm foreach (Star s in stars) { double a = s.Angle + rot; int x = (int)Math.Round(cx + Math.Cos(a) * s.R * radius * Cell.Aspect); int y = (int)Math.Round(cy + Math.Sin(a) * s.R * radius); if (x < 0 || y < 0 || x >= w || y >= h) continue; double b = s.Bright * (1 - twinkle * 0.5 * (1 + Math.Sin(t * s.TwinkleSpeed + s.TwinklePhase))); light[y * w + x] += b; if (glow > 0) { double g = b * glow * 0.15; if (x > 0) light[y * w + x - 1] += g; if (x < w - 1) light[y * w + x + 1] += g; if (y > 0) light[(y - 1) * w + x] += g; if (y < h - 1) light[(y + 1) * w + x] += g; } } for (int k = 0; k < w * h; k++) { double v = 1 - Math.Exp(-light[k] * 0.6); int idx = (int)(v * (Charset.Length - 1) + 0.5); chars[k] = Charset[idx]; colors[k] = idx == 0 ? -1 : (int)(v * (Shades - 1)); } } List<Star> MakeStars(int count) { List<Star> result = new List<Star>(); for (int i = 0; i < Math.Max(300, count); i++) { Star s = new Star(); if (rng.NextDouble() < 0.2) { // Faint stars scattered between the arms s.R = Math.Sqrt(rng.NextDouble()); s.Angle = rng.NextDouble() * 2 * Math.PI; s.Bright = 0.15 + 0.25 * rng.NextDouble(); } else { // Arm stars: the further out, the more the arm has curled back (a trailing spiral) int arm = rng.Next(arms); s.R = 0.05 + 0.95 * Math.Pow(rng.NextDouble(), 0.85); double spread = (0.08 + noise * 1.2) * (1.2 - 0.5 * s.R) * Gauss(); s.Angle = arm * 2 * Math.PI / arms - twist * 9 * s.R + spread; s.R = Math.Max(0.02, s.R + noise * 0.08 * Gauss()); s.Bright = (0.35 + 0.65 * rng.NextDouble()) * (1.1 - 0.6 * s.R); } s.TwinkleSpeed = 1.5 + 4 * rng.NextDouble(); s.TwinklePhase = rng.NextDouble() * 2 * Math.PI; result.Add(s); } return result; } double Gauss() { return Math.Sqrt(-2 * Math.Log(1 - rng.NextDouble())) * Math.Cos(2 * Math.PI * rng.NextDouble()); } } } |