animations/fire/fire.cs

using System;
using System.Collections.Generic;
 
namespace AsciiFX.Animations
{
    // A wall of fire. Heat rises from fuel rows at the bottom, drifts with the wind and cools as it climbs,
    // while embers float up slowly and sparks shoot out of the flames.
    public class Fire : IAnimation
    {
        class Particle { public double X, Y, VX, VY, Life; public bool Spark; }
 
        const int Shades = 32;
        const string Charset = " .'`,:;*+xX#"; // coolest to hottest; index 0 is empty air
 
        static readonly int[] Blue = { 0x0a0f32, 0x142d82, 0x235ad2, 0x4696fa, 0x8ccdff, 0xdcf5ff, 0xffffff };
        static readonly int[] Mocha = { 0x181825, 0x45475a, 0xcba6f7, 0xf38ba8, 0xfab387, 0xf9e2af, 0xf5e0dc }; // Catppuccin
 
        readonly int[] palette; // Shades heat colors, then the ember color, then the spark color
        readonly double intensity, wind, decay, turbulence;
        readonly int thickness;
        readonly bool embers, sparks, pulse;
        readonly Random rng = new Random();
        readonly List<Particle> particles = new List<Particle>();
        int w, h;
        double[] heat, next;
        double last;
 
        public Fire(Options o)
        {
            int[] stops = o.Text("Palette", "blue") == "mocha" ? Mocha : Blue;
            palette = new int[Shades + 2];
            Array.Copy(Colors.Gradient(Shades, stops), palette, Shades);
            palette[Shades] = Colors.At(0.7, stops);
            palette[Shades + 1] = stops[stops.Length - 1];
 
            intensity = o.Number("Intensity", 8);
            wind = o.Number("Wind", 4);
            decay = o.Number("Decay", 2.2);
            turbulence = o.Number("Turbulence", 8);
            thickness = (int)o.Number("Thickness", 3);
            embers = !o.Flag("NoEmbers");
            sparks = !o.Flag("NoSparks");
            pulse = !o.Flag("NoPulse");
        }
 
        public int[] Palette { get { return palette; } }
        public int FrameMs { get { return 22; } }
 
        public void Resize(int cols, int rows)
        {
            w = cols; h = rows;
            heat = new double[w * h];
            next = new double[w * h];
            particles.Clear();
        }
 
        public void Frame(double t, char[] chars, int[] colors)
        {
            double dt = Math.Min(0.1, Math.Max(0, t - last));
            last = t;
 
            // Refuel the bottom rows, with random hot spots and a slow pulse
            int fuelRows = Math.Max(1, Math.Min(thickness, h / 3));
            int top = h - fuelRows;
            double flicker = pulse ? 1 + 0.12 * Math.Sin(t * 3.4) + 0.05 * Math.Sin(t * 7.9) : 1;
            double fuel = Math.Min(1.0, 0.45 + intensity * 0.07) * flicker;
            for (int k = top * w; k < h * w; k++)
                heat[k] = rng.NextDouble() < 0.04 ? 1.2 : fuel * (0.65 + 0.35 * rng.NextDouble());
 
            // Each cell takes heat from just below it, shifted by wind and turbulence, then cools
            double cool = decay / (2.2 * 0.75 * Math.Max(1, top));
            double drift = wind * 0.12, jitter = turbulence * 0.18;
            for (int y = 0; y < top; y++)
            {
                int below = (y + 1) * w;
                for (int x = 0; x < w; x++)
                {
                    int sx = x - (int)Math.Round(drift + (rng.NextDouble() - 0.5) * jitter);
                    sx = ((sx % w) + w) % w;
                    int l = sx == 0 ? w - 1 : sx - 1, r = sx == w - 1 ? 0 : sx + 1;
                    double v = heat[below + sx] * 0.6 + heat[below + l] * 0.2 + heat[below + r] * 0.2
                               - cool * (0.4 + 1.2 * rng.NextDouble());
                    next[y * w + x] = v > 0 ? v : 0;
                }
            }
            Array.Copy(heat, top * w, next, top * w, fuelRows * w);
            double[] swap = heat; heat = next; next = swap;
 
            for (int k = 0; k < w * h; k++)
            {
                double v = Math.Min(1.0, heat[k]);
                if (v < 0.05) { chars[k] = ' '; colors[k] = -1; continue; }
                chars[k] = Charset[1 + (int)((v - 0.05) / 0.95 * (Charset.Length - 2))];
                colors[k] = (int)(v * (Shades - 1));
            }
 
            // Embers drift up slowly; sparks shoot up fast and burn out
            if (embers) Spawn(w * 0.18 * intensity / 8.0 * dt, false, top);
            if (sparks) Spawn(w * 0.06 * intensity / 8.0 * dt, true, top);
            for (int i = particles.Count - 1; i >= 0; i--)
            {
                Particle p = particles[i];
                p.Life -= dt;
                p.VX += (rng.NextDouble() - 0.5) * turbulence * 0.8 * dt;
                p.X += p.VX * dt; p.Y += p.VY * dt;
                if (p.X < 0) p.X += w;
                if (p.X >= w) p.X -= w;
                if (p.Life <= 0 || p.Y < 0) { particles.RemoveAt(i); continue; }
                int px = Math.Min(w - 1, (int)p.X), py = (int)p.Y;
                if (py >= h) continue;
                chars[py * w + px] = p.Spark ? (rng.NextDouble() < 0.5 ? '*' : '+') : (rng.NextDouble() < 0.5 ? '.' : '\'');
                colors[py * w + px] = p.Spark ? Shades + 1 : Shades;
            }
        }
 
        void Spawn(double expected, bool spark, int top)
        {
            for (double n = expected; n > 0; n -= 1)
            {
                if (n < 1 && rng.NextDouble() >= n) break;
                Particle p = new Particle();
                p.Spark = spark;
                p.X = rng.NextDouble() * w;
                p.Y = top - rng.NextDouble() * top * 0.35;
                p.VX = wind * 0.8 + (rng.NextDouble() - 0.5) * 3;
                p.VY = spark ? -(14 + rng.NextDouble() * 10) : -(4 + rng.NextDouble() * 5);
                p.Life = spark ? 0.4 + rng.NextDouble() * 0.5 : 1.2 + rng.NextDouble() * 1.8;
                particles.Add(p);
            }
        }
    }
}