core/AsciiFX.Picture.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
 
namespace AsciiFX.Animations
{
    // Plays a picture: the frames of a GIF or animated WebP turned into pixels by tools/New-PictureAnimation.py.
    // Each character cell shows two pixels stacked vertically, using a half block ('▀') in the upper pixel's color
    // over a background of the lower one's. The picture is scaled up by whole steps to fill the space it's given.
    //
    // The .picture file is plain text:
    // asciifx-picture 1
    // <width> <height> <frames> <milliseconds per frame>
    // then <height> lines per frame, each with <width> pixels: rrggbb, or . for transparent
    public class Picture : IBackgroundAnimation
    {
        const char Upper = '▀', Lower = '▄', Full = '█';
 
        readonly int width, height, frameMs;
        readonly int[][] frames; // per frame: palette index per pixel, -1 = transparent
        readonly int[] palette;
        readonly double speed;
        int w, h, scale, left, top;
        int[] scratch;
 
        public Picture(Options o)
        {
            string file = o.Text("File", "");
            if (!File.Exists(file)) throw new FileNotFoundException("Picture file not found: " + file);
            string[] lines = File.ReadAllLines(file);
            if (lines.Length < 2 || !lines[0].StartsWith("asciifx-picture")) throw new InvalidDataException("Not an AsciiFX picture: " + file);
 
            string[] header = lines[1].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            width = int.Parse(header[0]); height = int.Parse(header[1]);
            int count = int.Parse(header[2]);
            frameMs = Math.Max(20, int.Parse(header[3]));
            speed = Math.Max(0.05, o.Number("Speed", 1));
 
            Dictionary<int, int> index = new Dictionary<int, int>();
            List<int> colors = new List<int>();
            frames = new int[count][];
            for (int f = 0; f < count; f++)
            {
                frames[f] = new int[width * height];
                for (int y = 0; y < height; y++)
                {
                    string[] pixels = lines[2 + f * height + y].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                    for (int x = 0; x < width; x++)
                    {
                        if (x >= pixels.Length || pixels[x] == ".") { frames[f][y * width + x] = -1; continue; }
                        int rgb = int.Parse(pixels[x], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                        int i;
                        if (!index.TryGetValue(rgb, out i)) { i = colors.Count; index[rgb] = i; colors.Add(rgb); }
                        frames[f][y * width + x] = i;
                    }
                }
            }
            palette = colors.Count > 0 ? colors.ToArray() : new int[] { 0xffffff };
        }
 
        public int[] Palette { get { return palette; } }
        public int FrameMs { get { return frameMs; } }
 
        public void Resize(int cols, int rows)
        {
            w = cols; h = rows;
            scratch = new int[w * h];
            // Biggest whole-number scale that fits (cells are one pixel wide and two pixels tall), centered
            scale = Math.Max(1, Math.Min(w / width, rows * 2 / height));
            left = (w - width * scale) / 2;
            top = (rows * 2 - height * scale) / 2;
        }
 
        public void Frame(double t, char[] chars, int[] colors)
        {
            Frame(t, chars, colors, scratch);
        }
 
        public void Frame(double t, char[] chars, int[] colors, int[] backgrounds)
        {
            int[] frame = frames[(int)(t * 1000 * speed / frameMs) % frames.Length];
            for (int cy = 0; cy < h; cy++)
                for (int cx = 0; cx < w; cx++)
                {
                    int k = cy * w + cx;
                    int upper = PixelAt(frame, cx, cy * 2), lower = PixelAt(frame, cx, cy * 2 + 1);
                    backgrounds[k] = -1;
                    if (upper < 0 && lower < 0) { chars[k] = ' '; colors[k] = -1; }
                    else if (upper == lower) { chars[k] = Full; colors[k] = upper; }
                    else if (lower < 0) { chars[k] = Upper; colors[k] = upper; }
                    else if (upper < 0) { chars[k] = Lower; colors[k] = lower; }
                    else { chars[k] = Upper; colors[k] = upper; backgrounds[k] = lower; }
                }
        }
 
        int PixelAt(int[] frame, int cx, int py)
        {
            int x = cx - left, y = py - top;
            if (x < 0 || y < 0) return -1;
            x /= scale; y /= scale;
            return x < width && y < height ? frame[y * width + x] : -1;
        }
    }
}