animations/stars/stars.cs

using System;
using System.Collections.Generic;
 
namespace AsciiFX.Animations
{
    // A quiet night sky: sparse stars that slowly twinkle, thinning toward the bottom (as much as Fade says),
    // with a faint shooting star now and then.
    public class Stars : IAnimation
    {
        class Star { public int X, Y; public double Bright, Speed, Phase; }
        class Meteor { public double X, Y, VX, VY, Born, Life; }
 
        // Catppuccin surface1, overlay0, subtext0 and text: dim enough to stay in the background
        static readonly int[] palette = { 0x45475a, 0x6c7086, 0xa6adc8, 0xcdd6f4 };
 
        readonly double density, fade;
        readonly bool shooting;
        readonly Random rng = new Random();
        readonly List<Star> stars = new List<Star>();
        int w, h;
        Meteor meteor;
        double nextMeteor;
 
        public Stars(Options o)
        {
            density = Math.Max(0, o.Number("Density", 1));
            fade = Math.Max(0, Math.Min(1, o.Number("Fade", 0.7)));
            shooting = !o.Flag("NoShootingStars");
            nextMeteor = 15 + rng.NextDouble() * 30;
        }
 
        public int[] Palette { get { return palette; } }
        public int FrameMs { get { return 50; } }
 
        public void Resize(int cols, int rows)
        {
            w = cols; h = rows;
            stars.Clear();
            for (int y = 0; y < h; y++)
                for (int x = 0; x < w; x++)
                    if (rng.NextDouble() < 0.018 * density * (1 - fade * y / (double)h))
                    {
                        Star s = new Star();
                        s.X = x; s.Y = y;
                        s.Bright = 0.25 + 0.75 * Math.Pow(rng.NextDouble(), 2.5); // mostly faint, a few bright
                        s.Speed = 0.4 + rng.NextDouble() * 1.6;
                        s.Phase = rng.NextDouble() * 2 * Math.PI;
                        stars.Add(s);
                    }
        }
 
        public void Frame(double t, char[] chars, int[] colors)
        {
            for (int k = 0; k < w * h; k++) { chars[k] = ' '; colors[k] = -1; }
 
            foreach (Star s in stars)
            {
                double b = s.Bright * (0.6 + 0.4 * Math.Sin(t * s.Speed + s.Phase));
                int k = s.Y * w + s.X;
                chars[k] = b > 0.9 ? '*' : b > 0.65 ? '+' : b > 0.4 ? '\'' : '.';
                colors[k] = Math.Min(3, (int)(b * 4));
            }
 
            if (!shooting) return;
            if (meteor == null && t >= nextMeteor)
            {
                meteor = new Meteor();
                meteor.X = w * (0.1 + 0.8 * rng.NextDouble());
                meteor.Y = h * 0.4 * rng.NextDouble();
                meteor.VX = (rng.NextDouble() < 0.5 ? -1 : 1) * (35 + rng.NextDouble() * 20); // cells per second
                meteor.VY = 9 + rng.NextDouble() * 5;
                meteor.Born = t;
                meteor.Life = 0.9 + rng.NextDouble() * 0.5;
            }
            if (meteor == null) return;
 
            double age = t - meteor.Born;
            if (age > meteor.Life) { meteor = null; nextMeteor = t + 25 + rng.NextDouble() * 45; return; }
            double fade = Math.Sin(Math.PI * age / meteor.Life); // brightens, then fades out
            for (int i = 0; i < 8; i++) // the head, then a short tail along its path
            {
                double at = age - i * 0.025;
                if (at < 0) continue;
                int x = (int)(meteor.X + meteor.VX * at), y = (int)(meteor.Y + meteor.VY * at);
                if (x < 0 || y < 0 || x >= w || y >= h) continue;
                chars[y * w + x] = i == 0 ? '*' : i > 4 ? '.' : (meteor.VX > 0 ? '\\' : '/');
                colors[y * w + x] = (int)Math.Max(0, Math.Min(3, fade * (3.5 - i * 0.45)));
            }
        }
    }
}