animations/donut/donut.cs

using System;
 
namespace AsciiFX.Animations
{
    // A spinning, lit 3D torus. Points on the surface are rotated, projected with perspective and
    // shaded by how directly they face the light; a depth buffer lets the nearest surface win.
    public class Donut : IAnimation
    {
        const string Shades = ".,-~:;=!*#$@"; // darkest (facing away from the light) to brightest
 
        readonly int[] palette;
        readonly double rotX, rotY, size, thickness, lightAngle, lightHeight;
        int w, h;
        double[] depth;
 
        public Donut(Options o)
        {
            palette = new int[] { Colors.Parse(o.Text("Color", "#cdd6f4"), 0xcdd6f4) };
            rotX = o.Number("RotationX", 2);
            rotY = o.Number("RotationY", 1);
            size = o.Number("Size", 75);
            thickness = o.Number("Thickness", 50);
            lightAngle = o.Number("LightAngle", 45);
            lightHeight = o.Number("LightHeight", 45);
        }
 
        public int[] Palette { get { return palette; } }
        public int FrameMs { get { return 33; } }
 
        public void Resize(int cols, int rows)
        {
            w = cols; h = rows;
            depth = new double[w * h];
        }
 
        public void Frame(double t, char[] chars, int[] colors)
        {
            for (int k = 0; k < w * h; k++) { chars[k] = ' '; colors[k] = -1; depth[k] = 0; }
 
            // Tube radius r around a ring of radius 1; the camera sits far enough back to keep it all in view
            double r = Math.Max(1, Math.Min(100, thickness)) / 100.0, outer = 1 + r, camera = 2.5 * outer;
            double scale = size / 100.0 * Math.Min(h, w / Cell.Aspect) / 2 * camera / outer;
            double cx = (w - 1) / 2.0, cy = (h - 1) / 2.0;
 
            double a = t * rotX * 0.6, b = t * rotY * 0.6;
            double cosA = Math.Cos(a), sinA = Math.Sin(a), cosB = Math.Cos(b), sinB = Math.Sin(b);
 
            // Light direction: angle around the screen (0 = right, 90 = up), height toward the viewer
            double az = lightAngle * Math.PI / 180, el = lightHeight * Math.PI / 180;
            double lx = Math.Cos(el) * Math.Cos(az), ly = Math.Cos(el) * Math.Sin(az), lz = -Math.Sin(el);
 
            // Sample finely enough that neighboring points land well under a cell apart
            double cols = scale / (camera - outer) * Cell.Aspect;
            int ringSteps = (int)Math.Ceiling(2 * Math.PI * outer * cols * 2) + 8;
            int tubeSteps = (int)Math.Ceiling(2 * Math.PI * r * cols * 2) + 8;
 
            for (int i = 0; i < ringSteps; i++)
            {
                double phi = 2 * Math.PI * i / ringSteps, cosP = Math.Cos(phi), sinP = Math.Sin(phi);
                for (int j = 0; j < tubeSteps; j++)
                {
                    double theta = 2 * Math.PI * j / tubeSteps, cosT = Math.Cos(theta), sinT = Math.Sin(theta);
 
                    // Point and surface normal on a torus lying flat in the XY plane
                    double ring = 1 + r * cosT;
                    double px = ring * cosP, py = ring * sinP, pz = r * sinT;
                    double nx = cosT * cosP, ny = cosT * sinP, nz = sinT;
 
                    // Spin around X, then around Y
                    double py1 = py * cosA - pz * sinA, pz1 = py * sinA + pz * cosA;
                    double px2 = px * cosB + pz1 * sinB, pz2 = pz1 * cosB - px * sinB;
                    double ny1 = ny * cosA - nz * sinA, nz1 = ny * sinA + nz * cosA;
                    double nx2 = nx * cosB + nz1 * sinB, nz2 = nz1 * cosB - nx * sinB;
 
                    // Perspective projection, keeping only the surface nearest the viewer
                    double ooz = 1 / (camera + pz2);
                    int x = (int)Math.Round(cx + scale * px2 * ooz * Cell.Aspect);
                    int y = (int)Math.Round(cy - scale * py1 * ooz);
                    if (x < 0 || y < 0 || x >= w || y >= h) continue;
                    int k = y * w + x;
                    if (ooz <= depth[k]) continue;
                    depth[k] = ooz;
 
                    double lum = nx2 * lx + ny1 * ly + nz2 * lz;
                    chars[k] = Shades[lum > 0 ? Math.Min(Shades.Length - 1, (int)(lum * Shades.Length)) : 0];
                    colors[k] = 0;
                }
            }
        }
    }
}