The personal website of Scott W Harden
March 28th, 2023

Treemapping with C#

Treemap diagrams display a series of positive numbers using rectangles sized proportional to the value of each number. This page demonstrates how to calculate the size and location of rectangles to create a tree map diagram using C#. Although the following uses System.Drawing to save the tree map as a Bitmap image, these concepts may be combined with information on the C# Data Visualization page to create treemap diagrams using SkiaSharp, WPF, or other graphics technologies.

The tree map above was generated from random data using the following C# code:

// Create sample data. Data must be sorted large to small.
double[] sortedValues = Enumerable.Range(0, 40)
    .Select(x => (double)Random.Shared.Next(10, 100))
    .OrderByDescending(x => x)
    .ToArray();

// Create an array of labels in the same order as the sorted data.
string[] labels = sortedValues.Select(x => x.ToString()).ToArray();

// Calculate the size and position of all rectangles in the tree map
int width = 600;
int height = 400;
RectangleF[] rectangles = TreeMap.GetRectangles(sortedValues, width, height);

// Create an image to draw on (with 1px extra to make room for the outline)
using Bitmap bmp = new(width + 1, height + 1);
using Graphics gfx = Graphics.FromImage(bmp);
using Font fnt = new("Consolas", 8);
using SolidBrush brush = new(Color.Black);
gfx.Clear(Color.White);

// Draw and label each rectangle
for (int i = 0; i < rectangles.Length; i++)
{
    brush.Color = Color.FromArgb(
        red: Random.Shared.Next(150, 250),
        green: Random.Shared.Next(150, 250),
        blue: Random.Shared.Next(150, 250));

    gfx.FillRectangle(brush, rectangles[i]);
    gfx.DrawRectangle(Pens.Black, rectangles[i]);
    gfx.DrawString(labels[i], fnt, Brushes.Black, rectangles[i].X, rectangles[i].Y);
}

// Save the output
bmp.Save("treemap.bmp");

Treemap Logic

The previous code block focuses on data generation and display, but hides the tree map calculations behind the TreeMap class. Below is the code for that class. It is self-contained static class and exposes a single static method which takes a pre-sorted array of values and returns tree map rectangles ready to display on an image.

💡 Although the System.Drawing.Common is a Windows-only library (as of .NET 7), System.Drawing.Primitives is a cross-platform package that provides the RectangleF structure used in the tree map class. See the SkiaSharp Quickstart to learn how to create image files using cross-platform .NET code.

public static class TreeMap
{
    public static RectangleF[] GetRectangles(double[] values, int width, int height)
    {
        for (int i = 1; i < values.Length; i++)
            if (values[i] > values[i - 1])
                throw new ArgumentException("values must be ordered large to small");

        var slice = GetSlice(values, 1, 0.35);
        var rectangles = GetRectangles(slice, width, height);
        return rectangles.Select(x => x.ToRectF()).ToArray();
    }

    private class Slice
    {
        public double Size { get; }
        public IEnumerable<double> Values { get; }
        public Slice[] Children { get; }

        public Slice(double size, IEnumerable<double> values, Slice sub1, Slice sub2)
        {
            Size = size;
            Values = values;
            Children = new Slice[] { sub1, sub2 };
        }

        public Slice(double size, double finalValue)
        {
            Size = size;
            Values = new double[] { finalValue };
            Children = Array.Empty<Slice>();
        }
    }

    private class SliceResult
    {
        public double ElementsSize { get; }
        public IEnumerable<double> Elements { get; }
        public IEnumerable<double> RemainingElements { get; }

        public SliceResult(double elementsSize, IEnumerable<double> elements, IEnumerable<double> remainingElements)
        {
            ElementsSize = elementsSize;
            Elements = elements;
            RemainingElements = remainingElements;
        }
    }

    private class SliceRectangle
    {
        public Slice Slice { get; set; }
        public float X { get; set; }
        public float Y { get; set; }
        public float Width { get; set; }
        public float Height { get; set; }
        public SliceRectangle(Slice slice) => Slice = slice;
        public RectangleF ToRectF() => new(X, Y, Width, Height);
    }

    private static Slice GetSlice(IEnumerable<double> elements, double totalSize, double sliceWidth)
    {
        if (elements.Count() == 1)
            return new Slice(totalSize, elements.Single());

        SliceResult sr = GetElementsForSlice(elements, sliceWidth);
        Slice child1 = GetSlice(sr.Elements, sr.ElementsSize, sliceWidth);
        Slice child2 = GetSlice(sr.RemainingElements, 1 - sr.ElementsSize, sliceWidth);
        return new Slice(totalSize, elements, child1, child2);
    }

    private static SliceResult GetElementsForSlice(IEnumerable<double> elements, double sliceWidth)
    {
        var elementsInSlice = new List<double>();
        var remainingElements = new List<double>();
        double current = 0;
        double total = elements.Sum();

        foreach (var element in elements)
        {
            if (current > sliceWidth)
                remainingElements.Add(element);
            else
            {
                elementsInSlice.Add(element);
                current += element / total;
            }
        }

        return new SliceResult(current, elementsInSlice, remainingElements);
    }

    private static IEnumerable<SliceRectangle> GetRectangles(Slice slice, int width, int height)
    {
        SliceRectangle area = new(slice) { Width = width, Height = height };

        foreach (var rect in GetRectangles(area))
        {
            if (rect.X + rect.Width > area.Width)
                rect.Width = area.Width - rect.X;

            if (rect.Y + rect.Height > area.Height)
                rect.Height = area.Height - rect.Y;

            yield return rect;
        }
    }

    private static IEnumerable<SliceRectangle> GetRectangles(SliceRectangle sliceRectangle)
    {
        var isHorizontalSplit = sliceRectangle.Width >= sliceRectangle.Height;
        var currentPos = 0;
        foreach (var subSlice in sliceRectangle.Slice.Children)
        {
            var subRect = new SliceRectangle(subSlice);
            int rectSize;

            if (isHorizontalSplit)
            {
                rectSize = (int)Math.Round(sliceRectangle.Width * subSlice.Size);
                subRect.X = sliceRectangle.X + currentPos;
                subRect.Y = sliceRectangle.Y;
                subRect.Width = rectSize;
                subRect.Height = sliceRectangle.Height;
            }
            else
            {
                rectSize = (int)Math.Round(sliceRectangle.Height * subSlice.Size);
                subRect.X = sliceRectangle.X;
                subRect.Y = sliceRectangle.Y + currentPos;
                subRect.Width = sliceRectangle.Width;
                subRect.Height = rectSize;
            }

            currentPos += rectSize;

            if (subSlice.Values.Count() > 1)
            {
                foreach (var sr in GetRectangles(subRect))
                {
                    yield return sr;
                }
            }
            else if (subSlice.Values.Count() == 1)
            {
                yield return subRect;
            }
        }
    }
}

Source Code Complexity Analysis

A few days ago I wrote an article describing how to programmatically generate .NET source code analytics using C#. Using these tools I analyzed the source code for all classes in a large project (ScottPlot.NET). The following tree map displays every class in the project as a rectangle sized according to number of lines of code and colored according to maintainability.

In this diagram large rectangles represent classes with the most code, and red color indicates classes that are difficult to maintain.

💡 I'm using a perceptually uniform colormap (similar to Turbo)provided by the ScottPlot provided by the ScottPlot NuGet package. See ScottPlot's colormaps gallery for all available colormaps.

💡 The Maintainability Index is a value between 0 (worst) and 100 (best) that represents the relative ease of maintaining the code. It's calculated from a combination of Halstead complexity (size of the compiled code), Cyclomatic complexity (number of paths that can be taken through the code), and the total number of lines of code.

Conclusions

  • Generation of tree map diagrams can be achieved using recursive programming

  • The static class above makes it easy to generate tree maps in C#

  • ScottPlot's AxisTicksRender class may be difficult to maintain

References

Markdown source code last modified on March 8th, 2023
---
Title: Treemapping with C#
Description: How to create a treemap diagram using C#
Date: 2023-03-28 00:32AM EST
Tags: csharp, graphics
---

# Treemapping with C# 

**Treemap diagrams display a series of positive numbers using rectangles sized proportional to the value of each number.** This page demonstrates how to calculate the size and location of rectangles to create a tree map diagram using C#. Although the following uses System.Drawing to save the tree map as a Bitmap image, these concepts may be combined with information on the [C# Data Visualization](https://swharden.com/csdv/) page to create treemap diagrams using SkiaSharp, WPF, or other graphics technologies.

<img src="treemap.png" class="img-fluid d-block mx-auto shadow my-5">

The tree map above was generated from random data using the following C# code:

```cs
// Create sample data. Data must be sorted large to small.
double[] sortedValues = Enumerable.Range(0, 40)
    .Select(x => (double)Random.Shared.Next(10, 100))
    .OrderByDescending(x => x)
    .ToArray();

// Create an array of labels in the same order as the sorted data.
string[] labels = sortedValues.Select(x => x.ToString()).ToArray();

// Calculate the size and position of all rectangles in the tree map
int width = 600;
int height = 400;
RectangleF[] rectangles = TreeMap.GetRectangles(sortedValues, width, height);

// Create an image to draw on (with 1px extra to make room for the outline)
using Bitmap bmp = new(width + 1, height + 1);
using Graphics gfx = Graphics.FromImage(bmp);
using Font fnt = new("Consolas", 8);
using SolidBrush brush = new(Color.Black);
gfx.Clear(Color.White);

// Draw and label each rectangle
for (int i = 0; i < rectangles.Length; i++)
{
    brush.Color = Color.FromArgb(
        red: Random.Shared.Next(150, 250),
        green: Random.Shared.Next(150, 250),
        blue: Random.Shared.Next(150, 250));

    gfx.FillRectangle(brush, rectangles[i]);
    gfx.DrawRectangle(Pens.Black, rectangles[i]);
    gfx.DrawString(labels[i], fnt, Brushes.Black, rectangles[i].X, rectangles[i].Y);
}

// Save the output
bmp.Save("treemap.bmp");
```

## Treemap Logic

The previous code block focuses on data generation and display, but hides the tree map calculations behind the `TreeMap` class. Below is the code for that class. It is self-contained static class and exposes a single static method which takes a pre-sorted array of values and returns tree map rectangles ready to display on an image.

> 💡 Although the `System.Drawing.Common` is a Windows-only library ([as of .NET 7](https://github.com/dotnet/designs/blob/main/accepted/2021/system-drawing-win-only/system-drawing-win-only.md)), `System.Drawing.Primitives` is a cross-platform package that provides the `RectangleF` structure used in the tree map class. See the [SkiaSharp Quickstart](https://swharden.com/csdv/skiasharp/quickstart-console/) to learn how to create image files using cross-platform .NET code.

```cs
public static class TreeMap
{
    public static RectangleF[] GetRectangles(double[] values, int width, int height)
    {
        for (int i = 1; i < values.Length; i++)
            if (values[i] > values[i - 1])
                throw new ArgumentException("values must be ordered large to small");

        var slice = GetSlice(values, 1, 0.35);
        var rectangles = GetRectangles(slice, width, height);
        return rectangles.Select(x => x.ToRectF()).ToArray();
    }

    private class Slice
    {
        public double Size { get; }
        public IEnumerable<double> Values { get; }
        public Slice[] Children { get; }

        public Slice(double size, IEnumerable<double> values, Slice sub1, Slice sub2)
        {
            Size = size;
            Values = values;
            Children = new Slice[] { sub1, sub2 };
        }

        public Slice(double size, double finalValue)
        {
            Size = size;
            Values = new double[] { finalValue };
            Children = Array.Empty<Slice>();
        }
    }

    private class SliceResult
    {
        public double ElementsSize { get; }
        public IEnumerable<double> Elements { get; }
        public IEnumerable<double> RemainingElements { get; }

        public SliceResult(double elementsSize, IEnumerable<double> elements, IEnumerable<double> remainingElements)
        {
            ElementsSize = elementsSize;
            Elements = elements;
            RemainingElements = remainingElements;
        }
    }

    private class SliceRectangle
    {
        public Slice Slice { get; set; }
        public float X { get; set; }
        public float Y { get; set; }
        public float Width { get; set; }
        public float Height { get; set; }
        public SliceRectangle(Slice slice) => Slice = slice;
        public RectangleF ToRectF() => new(X, Y, Width, Height);
    }

    private static Slice GetSlice(IEnumerable<double> elements, double totalSize, double sliceWidth)
    {
        if (elements.Count() == 1)
            return new Slice(totalSize, elements.Single());

        SliceResult sr = GetElementsForSlice(elements, sliceWidth);
        Slice child1 = GetSlice(sr.Elements, sr.ElementsSize, sliceWidth);
        Slice child2 = GetSlice(sr.RemainingElements, 1 - sr.ElementsSize, sliceWidth);
        return new Slice(totalSize, elements, child1, child2);
    }

    private static SliceResult GetElementsForSlice(IEnumerable<double> elements, double sliceWidth)
    {
        var elementsInSlice = new List<double>();
        var remainingElements = new List<double>();
        double current = 0;
        double total = elements.Sum();

        foreach (var element in elements)
        {
            if (current > sliceWidth)
                remainingElements.Add(element);
            else
            {
                elementsInSlice.Add(element);
                current += element / total;
            }
        }

        return new SliceResult(current, elementsInSlice, remainingElements);
    }

    private static IEnumerable<SliceRectangle> GetRectangles(Slice slice, int width, int height)
    {
        SliceRectangle area = new(slice) { Width = width, Height = height };

        foreach (var rect in GetRectangles(area))
        {
            if (rect.X + rect.Width > area.Width)
                rect.Width = area.Width - rect.X;

            if (rect.Y + rect.Height > area.Height)
                rect.Height = area.Height - rect.Y;

            yield return rect;
        }
    }

    private static IEnumerable<SliceRectangle> GetRectangles(SliceRectangle sliceRectangle)
    {
        var isHorizontalSplit = sliceRectangle.Width >= sliceRectangle.Height;
        var currentPos = 0;
        foreach (var subSlice in sliceRectangle.Slice.Children)
        {
            var subRect = new SliceRectangle(subSlice);
            int rectSize;

            if (isHorizontalSplit)
            {
                rectSize = (int)Math.Round(sliceRectangle.Width * subSlice.Size);
                subRect.X = sliceRectangle.X + currentPos;
                subRect.Y = sliceRectangle.Y;
                subRect.Width = rectSize;
                subRect.Height = sliceRectangle.Height;
            }
            else
            {
                rectSize = (int)Math.Round(sliceRectangle.Height * subSlice.Size);
                subRect.X = sliceRectangle.X;
                subRect.Y = sliceRectangle.Y + currentPos;
                subRect.Width = sliceRectangle.Width;
                subRect.Height = rectSize;
            }

            currentPos += rectSize;

            if (subSlice.Values.Count() > 1)
            {
                foreach (var sr in GetRectangles(subRect))
                {
                    yield return sr;
                }
            }
            else if (subSlice.Values.Count() == 1)
            {
                yield return subRect;
            }
        }
    }
}
```

## Source Code Complexity Analysis

A few days ago I wrote an article describing how to [programmatically generate .NET source code analytics using C#](https://swharden.com/blog/2023-03-05-dotnet-code-analysis/). Using these tools I analyzed the source code for all classes in a large project ([ScottPlot.NET](https://scottplot.net)). The following tree map displays every class in the project as a rectangle sized according to number of lines of code and colored according to maintainability. 

<a href="code-report.png"><img src="code-report.png" class="img-fluid d-inline-block mx-auto shadow mt-5"></a>

<img src="turbo.png" class="img-fluid d-inline-block mx-auto shadow mb-5">

In this diagram large rectangles represent classes with the most code, and red color indicates classes that are difficult to maintain. 

> 💡 I'm using a perceptually uniform colormap (similar to [Turbo](https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html))provided by the [ScottPlot](https://scottplot.net) provided by the [ScottPlot](https://scottplot.net) NuGet package. See ScottPlot's [colormaps gallery](https://scottplot.net/cookbook/4.1/colormaps/) for all available colormaps.

> 💡 The [Maintainability Index](https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metrics-maintainability-index-range-and-meaning) is a value between 0 (worst) and 100 (best) that represents the relative ease of maintaining the code. It's calculated from a combination of [Halstead complexity](https://en.wikipedia.org/wiki/Halstead_complexity_measures) (size of the compiled code), [Cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) (number of paths that can be taken through the code), and the total number of lines of code.

## Conclusions

* Generation of tree map diagrams can be achieved using recursive programming

* The static class above makes it easy to generate tree maps in C#

* ScottPlot's AxisTicksRender class may be difficult to maintain

## References

* This blog post is spillover from ScottPlot issues [#1479](https://github.com/ScottPlot/ScottPlot/issues/1479) and [#2454](https://github.com/ScottPlot/ScottPlot/issues/2454).

* Code here was heavily influenced by [The Never Ending Journey](http://pascallaurin42.blogspot.com/2013/12/implementing-treemap-in-c.html) (Dec 29, 2013)

* [Treemapping](https://en.wikipedia.org/wiki/Treemapping) (Wikipedia)

* [D3 Treemap](https://d3-graph-gallery.com/treemap.html)

* [Squarified Treemaps](https://www.win.tue.nl/~vanwijk/stm.pdf) (Bruls et al.)

* StackOverflow question [32548949](https://stackoverflow.com/questions/32548949/from-c-sharp-serverside-is-there-anyway-to-generate-a-treemap-and-save-as-an-im/37154938#37154938)

* [C# Data Visualization](https://swharden.com/csdv/)
November 7th, 2022

Creating Bitmaps from Scratch in C#

This project how to represent bitmap data in a plain old C object (POCO) to create images from scratch using C# and no dependencies. Common graphics libraries like SkiaSharp, ImageSharp, System.Drawing, and Maui.Graphics can read and write bitmaps in memory, so a POCO that stores image data and converts it to a bitmap byte allows creation of platform-agnostic APIs that can be interfaced from any graphics library.

This page demonstrates how to use C# (.NET 6.0) to create bitmap images from scratch. Bitmap images can then be saved to disk and viewed with any image editing program, or they can consumed as a byte array in memory by a graphics library. There are various bitmap image formats (grayscale, indexed colors, 16-bit, 32-bit, transparent, etc.) but code here demonstrates the simplest common case (8-bit RGB color).

Representing Color

The following struct represents RGB color as 3 byte values and has helper methods for creating new colors.

public struct RawColor
{
    public readonly byte R, G, B;

    public RawColor(byte r, byte g, byte b)
    {
        (R, G, B) = (r, g, b);
    }

    public static RawColor Random(Random rand)
    {
        byte r = (byte)rand.Next(256);
        byte g = (byte)rand.Next(256);
        byte b = (byte)rand.Next(256);
        return new RawColor(r, g, b);
    }

    public static RawColor Gray(byte value)
    {
        return new RawColor(value, value, value);
    }
}

A color class like this could be extended to support additional niceties. Refer to SkiaSharp's SKColor.cs, System.Drawing's Color.cs, and Maui.Graphics' Color.cs for examples and implementation details. I commonly find the following features useful include when writing a color class:

  • A static class with named colors e.g., RawColors.Blue
  • Conversion to/from ARGB e.g., RawColor.FromAGRB(123456)
  • Conversion to/from HTML e.g., RawColor.FromHtml(#003366)
  • Conversion between RGB and HSL/HSV
  • Helper functions to Lighten() and Darken()
  • Helper functions to ShiftHue()
  • Extension methods to convert to common other formats like SKColor

Representing the Bitmap Image

This is the entire image class and it serves a few specific roles:

  • Store image data in a byte array arranged identically to how it will be exported in the bitmap
  • Provide helper methods to get/set pixel color
  • Provide a method to return the image as a bitmap by adding a minimal header
public class RawBitmap
{
    public readonly int Width;
    public readonly int Height;
    private readonly byte[] ImageBytes;

    public RawBitmap(int width, int height)
    {
        Width = width;
        Height = height;
        ImageBytes = new byte[width * height * 4];
    }

    public void SetPixel(int x, int y, RawColor color)
    {
        int offset = ((Height - y - 1) * Width + x) * 4;
        ImageBytes[offset + 0] = color.B;
        ImageBytes[offset + 1] = color.G;
        ImageBytes[offset + 2] = color.R;
    }

    public byte[] GetBitmapBytes()
    {
        const int imageHeaderSize = 54;
        byte[] bmpBytes = new byte[ImageBytes.Length + imageHeaderSize];
        bmpBytes[0] = (byte)'B';
        bmpBytes[1] = (byte)'M';
        bmpBytes[14] = 40;
        Array.Copy(BitConverter.GetBytes(bmpBytes.Length), 0, bmpBytes, 2, 4);
        Array.Copy(BitConverter.GetBytes(imageHeaderSize), 0, bmpBytes, 10, 4);
        Array.Copy(BitConverter.GetBytes(Width), 0, bmpBytes, 18, 4);
        Array.Copy(BitConverter.GetBytes(Height), 0, bmpBytes, 22, 4);
        Array.Copy(BitConverter.GetBytes(32), 0, bmpBytes, 28, 2);
        Array.Copy(BitConverter.GetBytes(ImageBytes.Length), 0, bmpBytes, 34, 4);
        Array.Copy(ImageBytes, 0, bmpBytes, imageHeaderSize, ImageBytes.Length);
        return bmpBytes;
    }

    public void Save(string filename)
    {
        byte[] bytes = GetBitmapBytes();
        File.WriteAllBytes(filename, bytes);
    }
}

Generate Images from Scratch

The following code uses the bitmap class and color struct above to create test images

Random Colors

RawBitmap bmp = new(400, 300);
Random rand = new();
for (int x = 0; x < bmp.Width; x++)
    for (int y = 0; y < bmp.Height; y++)
        bmp.SetPixel(x, y, RawColor.Random(rand));
bmp.Save("random-rgb.bmp");

Rainbow

RawBitmap bmp = new(400, 300);
Random rand = new();
for (int x = 0; x < bmp.Width; x++)
{
    for (int y = 0; y < bmp.Height; y++)
    {
        byte r = (byte)(255.0 * x / bmp.Width);
        byte g = (byte)(255.0 * y / bmp.Height);
        byte b = (byte)(255 - 255.0 * x / bmp.Width);
        RawColor color = new(r, g, b);
        bmp.SetPixel(x, y, color);
    }
}
bmp.Save("rainbow.bmp");

Rectangles

RawBitmap bmp = new(400, 300);
Random rand = new();
for (int i = 0; i < 1000; i++)
{
    int rectX = rand.Next(bmp.Width);
    int rectY = rand.Next(bmp.Height);
    int rectWidth = rand.Next(50);
    int rectHeight = rand.Next(50);
    RawColor color = RawColor.Random(rand);

    for (int x = rectX; x < rectX + rectWidth; x++)
    {
        for (int y = rectY; y < rectY + rectHeight; y++)
        {
            if (x < 0 || x >= bmp.Width) continue;
            if (y < 0 || y >= bmp.Height) continue;
            bmp.SetPixel(x, y, color);
        }
    }
}
bmp.Save("rectangles.bmp");

Interfacing Graphics Libraries

The following code demonstrates how to load the bitmap byte arrays generated above into common graphics libraries and save the result as a JPEG file. Although the bitmap byte array can be written directly to disk as a .bmp file, these third-party libraries are required to encode images in additional formats like JPEG.

System.Drawing

using System.Drawing;

static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
    using MemoryStream ms = new(bytes);
    using Image img = Bitmap.FromStream(ms);
    img.Save(filename);
}

SkiaSharp

using SkiaSharp;

static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
    using SKBitmap bmp = SKBitmap.Decode(bytes);
    using SKFileWStream fs = new(filename);
    bmp.Encode(fs, SKEncodedImageFormat.Jpeg, quality: 95);
}

ImageSharp

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;

static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
    using Image img = Image.Load(bytes);
    JpegEncoder encoder = new() { Quality = 95 };
    img.Save(filename, encoder);
}

Resources

Markdown source code last modified on November 8th, 2022
---
Title: Creating Bitmaps from Scratch in C#
Description: How to create a bitmap and set pixel colors in memory and save the result to disk or convert it to a traditional image format
Date: 2022-11-07 18:45PM EST
Tags: csharp, graphics
---

# Creating Bitmaps from Scratch in C# 

**This project how to represent bitmap data in a plain old C object (POCO) to create images from scratch using C# and no dependencies.** 
Common graphics libraries like [SkiaSharp](https://swharden.com/csdv/skiasharp/), [ImageSharp](https://swharden.com/csdv/platforms/imagesharp/), [System.Drawing](https://swharden.com/csdv/system.drawing/), and [Maui.Graphics](https://swharden.com/csdv/maui.graphics/) can read and write bitmaps in memory, so a [POCO](https://en.wikipedia.org/wiki/POCO) that stores image data and converts it to a bitmap byte allows creation of platform-agnostic APIs that can be interfaced from any graphics library.

**This page demonstrates how to use C# (.NET 6.0) to create bitmap images from scratch.** Bitmap images can then be saved to disk and viewed with any image editing program, or they can consumed as a byte array in memory by a graphics library. There are various [bitmap image formats](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/advanced/types-of-bitmaps?view=netframeworkdesktop-4.8) (grayscale, indexed colors, 16-bit, 32-bit, transparent, etc.) but code here demonstrates the simplest common case (8-bit RGB color).

## Representing Color

The following `struct` represents RGB color as 3 `byte` values and has helper methods for creating new colors. 

```cs
public struct RawColor
{
    public readonly byte R, G, B;

    public RawColor(byte r, byte g, byte b)
    {
        (R, G, B) = (r, g, b);
    }

    public static RawColor Random(Random rand)
    {
        byte r = (byte)rand.Next(256);
        byte g = (byte)rand.Next(256);
        byte b = (byte)rand.Next(256);
        return new RawColor(r, g, b);
    }

    public static RawColor Gray(byte value)
    {
        return new RawColor(value, value, value);
    }
}
```

A color class like this could be extended to support additional niceties. Refer to [SkiaSharp's `SKColor.cs`](https://github.com/mono/SkiaSharp/blob/main/binding/Binding/SKColor.cs), [System.Drawing's `Color.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Drawing.Primitives/src/System/Drawing/Color.cs), and [Maui.Graphics' `Color.cs`](https://github.com/dotnet/maui/blob/main/src/Graphics/src/Graphics/Color.cs) for examples and implementation details. I commonly find the following features useful include when writing a color class:

* A static class with named colors e.g., `RawColors.Blue`
* Conversion to/from ARGB e.g., `RawColor.FromAGRB(123456)`
* Conversion to/from HTML e.g., `RawColor.FromHtml(#003366)`
* Conversion between RGB and [HSL/HSV](https://en.wikipedia.org/wiki/HSL_and_HSV)
* Helper functions to `Lighten()` and `Darken()`
* Helper functions to `ShiftHue()`
* Extension methods to convert to common other formats like `SKColor`

## Representing the Bitmap Image

This is the entire image class and it serves a few specific roles:

* Store image data in a byte array arranged identically to how it will be exported in the bitmap
* Provide helper methods to get/set pixel color
* Provide a method to return the image as a bitmap by adding a minimal header

```cs
public class RawBitmap
{
    public readonly int Width;
    public readonly int Height;
    private readonly byte[] ImageBytes;

    public RawBitmap(int width, int height)
    {
        Width = width;
        Height = height;
        ImageBytes = new byte[width * height * 4];
    }

    public void SetPixel(int x, int y, RawColor color)
    {
        int offset = ((Height - y - 1) * Width + x) * 4;
        ImageBytes[offset + 0] = color.B;
        ImageBytes[offset + 1] = color.G;
        ImageBytes[offset + 2] = color.R;
    }

    public byte[] GetBitmapBytes()
    {
        const int imageHeaderSize = 54;
        byte[] bmpBytes = new byte[ImageBytes.Length + imageHeaderSize];
        bmpBytes[0] = (byte)'B';
        bmpBytes[1] = (byte)'M';
        bmpBytes[14] = 40;
        Array.Copy(BitConverter.GetBytes(bmpBytes.Length), 0, bmpBytes, 2, 4);
        Array.Copy(BitConverter.GetBytes(imageHeaderSize), 0, bmpBytes, 10, 4);
        Array.Copy(BitConverter.GetBytes(Width), 0, bmpBytes, 18, 4);
        Array.Copy(BitConverter.GetBytes(Height), 0, bmpBytes, 22, 4);
        Array.Copy(BitConverter.GetBytes(32), 0, bmpBytes, 28, 2);
        Array.Copy(BitConverter.GetBytes(ImageBytes.Length), 0, bmpBytes, 34, 4);
        Array.Copy(ImageBytes, 0, bmpBytes, imageHeaderSize, ImageBytes.Length);
        return bmpBytes;
    }

    public void Save(string filename)
    {
        byte[] bytes = GetBitmapBytes();
        File.WriteAllBytes(filename, bytes);
    }
}
```

## Generate Images from Scratch

The following code uses the bitmap class and color struct above to create test images

### Random Colors

```cs
RawBitmap bmp = new(400, 300);
Random rand = new();
for (int x = 0; x < bmp.Width; x++)
    for (int y = 0; y < bmp.Height; y++)
        bmp.SetPixel(x, y, RawColor.Random(rand));
bmp.Save("random-rgb.bmp");
```

![](SkiaSharp-random-rgb.jpg)

### Rainbow
```cs
RawBitmap bmp = new(400, 300);
Random rand = new();
for (int x = 0; x < bmp.Width; x++)
{
    for (int y = 0; y < bmp.Height; y++)
    {
        byte r = (byte)(255.0 * x / bmp.Width);
        byte g = (byte)(255.0 * y / bmp.Height);
        byte b = (byte)(255 - 255.0 * x / bmp.Width);
        RawColor color = new(r, g, b);
        bmp.SetPixel(x, y, color);
    }
}
bmp.Save("rainbow.bmp");
```

![](SkiaSharp-rainbow.jpg)

### Rectangles
```cs
RawBitmap bmp = new(400, 300);
Random rand = new();
for (int i = 0; i < 1000; i++)
{
    int rectX = rand.Next(bmp.Width);
    int rectY = rand.Next(bmp.Height);
    int rectWidth = rand.Next(50);
    int rectHeight = rand.Next(50);
    RawColor color = RawColor.Random(rand);

    for (int x = rectX; x < rectX + rectWidth; x++)
    {
        for (int y = rectY; y < rectY + rectHeight; y++)
        {
            if (x < 0 || x >= bmp.Width) continue;
            if (y < 0 || y >= bmp.Height) continue;
            bmp.SetPixel(x, y, color);
        }
    }
}
bmp.Save("rectangles.bmp");
```

![](SkiaSharp-rectangles.jpg)

## Interfacing Graphics Libraries

**The following code demonstrates how to load the bitmap byte arrays generated above into common graphics libraries and save the result as a JPEG file.** Although the bitmap byte array can be written directly to disk as a .bmp file, these third-party libraries are required to encode images in additional formats like JPEG.


### System.Drawing

```cs
using System.Drawing;

static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
    using MemoryStream ms = new(bytes);
    using Image img = Bitmap.FromStream(ms);
    img.Save(filename);
}
```

### SkiaSharp

```cs
using SkiaSharp;

static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
    using SKBitmap bmp = SKBitmap.Decode(bytes);
    using SKFileWStream fs = new(filename);
    bmp.Encode(fs, SKEncodedImageFormat.Jpeg, quality: 95);
}
```

### ImageSharp

```cs
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;

static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
    using Image img = Image.Load(bytes);
    JpegEncoder encoder = new() { Quality = 95 };
    img.Save(filename, encoder);
}
```

# Resources

* [C# Data Visualization](https://swharden.com/csdv/) - Resources for visualizing data using C# and the .NET platform

* [SkiaSharp: `SKColor.cs`](https://github.com/mono/SkiaSharp/blob/main/binding/Binding/SKColor.cs)

* [System.Drawing: `Color.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Drawing.Primitives/src/System/Drawing/Color.cs)

* [Maui.Graphics: `Color.cs`](https://github.com/dotnet/maui/blob/main/src/Graphics/src/Graphics/Color.cs)
June 23rd, 2022

Resample Time Series Data using Cubic Spline Interpolation

Cubic spline interpolation can be used to modify the sample rate of time series data. This page describes how I achieve signal resampling with spline interpolation in pure C# without any external dependencies. This technique can be used to:

  • Convert unevenly-sampled data to a series of values with a fixed sample rate

  • Convert time series data from one sample rate to another sample rate

  • Fill-in missing values from a collection of measurements

Simulating Data Samples

To simulate unevenly-sampled data I create a theoretical signal then sample it at 20 random time points.

// this function represents the signal being measured
static double f(double x) => Math.Sin(x * 10) + Math.Sin(x * 13f);

// randomly sample values from 20 time points
Random rand = new(123);
double[] sampleXs = Enumerable.Range(0, 20)
    .Select(x => rand.NextDouble())
    .OrderBy(x => x)
    .ToArray();
double[] sampleYs = sampleXs.Select(x => f(x)).ToArray();

Resample for Evenly-Spaced Data

I then generate an interpolated spline using my sampled data points as the input.

  • I can control the sample rate by defining the number of points generated in the output signal.

  • Full source code is at the bottom of this article.

(double[] xs, double[] ys) = Cubic.Interpolate1D(sampleXs, sampleYs, count: 50);

The generated points line-up perfectly with the sampled data.

There is slight deviation from the theoretical signal (and it's larger where there is more missing data) but this is an unsurprising result considering the original samples had large gaps of missing data.

Source Code

Interpolation.cs

public static class Interpolation
{
    public static (double[] xs, double[] ys) Interpolate1D(double[] xs, double[] ys, int count)
    {
        if (xs is null || ys is null || xs.Length != ys.Length)
            throw new ArgumentException($"{nameof(xs)} and {nameof(ys)} must have same length");

        int inputPointCount = xs.Length;
        double[] inputDistances = new double[inputPointCount];
        for (int i = 1; i < inputPointCount; i++)
            inputDistances[i] = inputDistances[i - 1] + xs[i] - xs[i - 1];

        double meanDistance = inputDistances.Last() / (count - 1);
        double[] evenDistances = Enumerable.Range(0, count).Select(x => x * meanDistance).ToArray();
        double[] xsOut = Interpolate(inputDistances, xs, evenDistances);
        double[] ysOut = Interpolate(inputDistances, ys, evenDistances);
        return (xsOut, ysOut);
    }

    private static double[] Interpolate(double[] xOrig, double[] yOrig, double[] xInterp)
    {
        (double[] a, double[] b) = FitMatrix(xOrig, yOrig);

        double[] yInterp = new double[xInterp.Length];
        for (int i = 0; i < yInterp.Length; i++)
        {
            int j;
            for (j = 0; j < xOrig.Length - 2; j++)
                if (xInterp[i] <= xOrig[j + 1])
                    break;

            double dx = xOrig[j + 1] - xOrig[j];
            double t = (xInterp[i] - xOrig[j]) / dx;
            double y = (1 - t) * yOrig[j] + t * yOrig[j + 1] +
                t * (1 - t) * (a[j] * (1 - t) + b[j] * t);
            yInterp[i] = y;
        }

        return yInterp;
    }

    private static (double[] a, double[] b) FitMatrix(double[] x, double[] y)
    {
        int n = x.Length;
        double[] a = new double[n - 1];
        double[] b = new double[n - 1];
        double[] r = new double[n];
        double[] A = new double[n];
        double[] B = new double[n];
        double[] C = new double[n];

        double dx1, dx2, dy1, dy2;

        dx1 = x[1] - x[0];
        C[0] = 1.0f / dx1;
        B[0] = 2.0f * C[0];
        r[0] = 3 * (y[1] - y[0]) / (dx1 * dx1);

        for (int i = 1; i < n - 1; i++)
        {
            dx1 = x[i] - x[i - 1];
            dx2 = x[i + 1] - x[i];
            A[i] = 1.0f / dx1;
            C[i] = 1.0f / dx2;
            B[i] = 2.0f * (A[i] + C[i]);
            dy1 = y[i] - y[i - 1];
            dy2 = y[i + 1] - y[i];
            r[i] = 3 * (dy1 / (dx1 * dx1) + dy2 / (dx2 * dx2));
        }

        dx1 = x[n - 1] - x[n - 2];
        dy1 = y[n - 1] - y[n - 2];
        A[n - 1] = 1.0f / dx1;
        B[n - 1] = 2.0f * A[n - 1];
        r[n - 1] = 3 * (dy1 / (dx1 * dx1));

        double[] cPrime = new double[n];
        cPrime[0] = C[0] / B[0];
        for (int i = 1; i < n; i++)
            cPrime[i] = C[i] / (B[i] - cPrime[i - 1] * A[i]);

        double[] dPrime = new double[n];
        dPrime[0] = r[0] / B[0];
        for (int i = 1; i < n; i++)
            dPrime[i] = (r[i] - dPrime[i - 1] * A[i]) / (B[i] - cPrime[i - 1] * A[i]);

        double[] k = new double[n];
        k[n - 1] = dPrime[n - 1];
        for (int i = n - 2; i >= 0; i--)
            k[i] = dPrime[i] - cPrime[i] * k[i + 1];

        for (int i = 1; i < n; i++)
        {
            dx1 = x[i] - x[i - 1];
            dy1 = y[i] - y[i - 1];
            a[i - 1] = k[i - 1] * dx1 - dy1;
            b[i - 1] = -k[i] * dx1 + dy1;
        }

        return (a, b);
    }
}

Program.cs

This is the source code I used to generate the figures on this page.

Plots were generated using ScottPlot.NET.

// this function represents the signal being measured
static double f(double x) => Math.Sin(x * 10) + Math.Sin(x * 13f);

// create points representing randomly sampled time points of a smooth curve
Random rand = new(123);
double[] sampleXs = Enumerable.Range(0, 20)
    .Select(x => rand.NextDouble())
    .OrderBy(x => x)
    .ToArray();
double[] sampleYs = sampleXs.Select(x => f(x)).ToArray();

// use 1D interpolation to create an evenly sampled curve from unevenly sampled data
(double[] xs, double[] ys) = Interpolation.Interpolate1D(sampleXs, sampleYs, count: 50);

var plt = new ScottPlot.Plot(600, 400);

double[] theoreticalXs = ScottPlot.DataGen.Range(xs.Min(), xs.Max(), .01);
double[] theoreticalYs = theoreticalXs.Select(x => f(x)).ToArray();
var perfectPlot = plt.AddScatterLines(theoreticalXs, theoreticalYs);
perfectPlot.Label = "theoretical signal";
perfectPlot.Color = plt.Palette.GetColor(2);
perfectPlot.LineStyle = ScottPlot.LineStyle.Dash;

var samplePlot = plt.AddScatterPoints(sampleXs, sampleYs);
samplePlot.Label = "sampled points";
samplePlot.Color = plt.Palette.GetColor(0);
samplePlot.MarkerSize = 10;
samplePlot.MarkerShape = ScottPlot.MarkerShape.openCircle;
samplePlot.MarkerLineWidth = 2;

var smoothPlot = plt.AddScatter(xs, ys);
smoothPlot.Label = "interpolated points";
smoothPlot.Color = plt.Palette.GetColor(3);
smoothPlot.MarkerShape = ScottPlot.MarkerShape.filledCircle;

plt.Legend();
plt.SaveFig("output.png");

2D and 3D Spline Interpolation

The interpolation method described above only considered the horizontal axis when generating evenly-spaced time points (1D interpolation). For information and code examples regarding 2D and 3D cubic spline interpolation, see my previous blog post: Spline Interpolation with C#

Resources

Markdown source code last modified on June 24th, 2022
---
Title: Resample Time Series Data using Cubic Spline Interpolation
Description: This page describes how I achieve signal resampling with spline interpolation in pure C# without any external dependencies.
Date: 2022-06-23 21:15PM EST
Tags: csharp, graphics
---

# Resample Time Series Data using Cubic Spline Interpolation

**Cubic spline interpolation can be used to modify the sample rate of time series data.** This page describes how I achieve signal resampling with spline interpolation in pure C# without any external dependencies. This technique can be used to:

* Convert unevenly-sampled data to a series of values with a fixed sample rate

* Convert time series data from one sample rate to another sample rate

* Fill-in missing values from a collection of measurements

### Simulating Data Samples

To simulate unevenly-sampled data I create a theoretical signal then sample it at 20 random time points.

```cs
// this function represents the signal being measured
static double f(double x) => Math.Sin(x * 10) + Math.Sin(x * 13f);

// randomly sample values from 20 time points
Random rand = new(123);
double[] sampleXs = Enumerable.Range(0, 20)
    .Select(x => rand.NextDouble())
    .OrderBy(x => x)
    .ToArray();
double[] sampleYs = sampleXs.Select(x => f(x)).ToArray();
```

<img src="1-samples-only.png" class="mx-auto d-block mb-5">

### Resample for Evenly-Spaced Data

I then generate an interpolated spline using my sampled data points as the input. 

* I can control the sample rate by defining the number of points generated in the output signal. 

* Full source code is at the bottom of this article.

```cs
(double[] xs, double[] ys) = Cubic.Interpolate1D(sampleXs, sampleYs, count: 50);
```

<img src="2-resample-only.png" class="mx-auto d-block mb-5">

The generated points line-up perfectly with the sampled data.

<img src="2-resample.png" class="mx-auto d-block mb-5">

There is slight deviation from the theoretical signal (and it's larger where there is more missing data) but this is an unsurprising result considering the original samples had large gaps of missing data.

<img src="3-comparison.png" class="mx-auto d-block mb-5">

## Source Code

### Interpolation.cs

```cs
public static class Interpolation
{
    public static (double[] xs, double[] ys) Interpolate1D(double[] xs, double[] ys, int count)
    {
        if (xs is null || ys is null || xs.Length != ys.Length)
            throw new ArgumentException($"{nameof(xs)} and {nameof(ys)} must have same length");

        int inputPointCount = xs.Length;
        double[] inputDistances = new double[inputPointCount];
        for (int i = 1; i < inputPointCount; i++)
            inputDistances[i] = inputDistances[i - 1] + xs[i] - xs[i - 1];

        double meanDistance = inputDistances.Last() / (count - 1);
        double[] evenDistances = Enumerable.Range(0, count).Select(x => x * meanDistance).ToArray();
        double[] xsOut = Interpolate(inputDistances, xs, evenDistances);
        double[] ysOut = Interpolate(inputDistances, ys, evenDistances);
        return (xsOut, ysOut);
    }

    private static double[] Interpolate(double[] xOrig, double[] yOrig, double[] xInterp)
    {
        (double[] a, double[] b) = FitMatrix(xOrig, yOrig);

        double[] yInterp = new double[xInterp.Length];
        for (int i = 0; i < yInterp.Length; i++)
        {
            int j;
            for (j = 0; j < xOrig.Length - 2; j++)
                if (xInterp[i] <= xOrig[j + 1])
                    break;

            double dx = xOrig[j + 1] - xOrig[j];
            double t = (xInterp[i] - xOrig[j]) / dx;
            double y = (1 - t) * yOrig[j] + t * yOrig[j + 1] +
                t * (1 - t) * (a[j] * (1 - t) + b[j] * t);
            yInterp[i] = y;
        }

        return yInterp;
    }

    private static (double[] a, double[] b) FitMatrix(double[] x, double[] y)
    {
        int n = x.Length;
        double[] a = new double[n - 1];
        double[] b = new double[n - 1];
        double[] r = new double[n];
        double[] A = new double[n];
        double[] B = new double[n];
        double[] C = new double[n];

        double dx1, dx2, dy1, dy2;

        dx1 = x[1] - x[0];
        C[0] = 1.0f / dx1;
        B[0] = 2.0f * C[0];
        r[0] = 3 * (y[1] - y[0]) / (dx1 * dx1);

        for (int i = 1; i < n - 1; i++)
        {
            dx1 = x[i] - x[i - 1];
            dx2 = x[i + 1] - x[i];
            A[i] = 1.0f / dx1;
            C[i] = 1.0f / dx2;
            B[i] = 2.0f * (A[i] + C[i]);
            dy1 = y[i] - y[i - 1];
            dy2 = y[i + 1] - y[i];
            r[i] = 3 * (dy1 / (dx1 * dx1) + dy2 / (dx2 * dx2));
        }

        dx1 = x[n - 1] - x[n - 2];
        dy1 = y[n - 1] - y[n - 2];
        A[n - 1] = 1.0f / dx1;
        B[n - 1] = 2.0f * A[n - 1];
        r[n - 1] = 3 * (dy1 / (dx1 * dx1));

        double[] cPrime = new double[n];
        cPrime[0] = C[0] / B[0];
        for (int i = 1; i < n; i++)
            cPrime[i] = C[i] / (B[i] - cPrime[i - 1] * A[i]);

        double[] dPrime = new double[n];
        dPrime[0] = r[0] / B[0];
        for (int i = 1; i < n; i++)
            dPrime[i] = (r[i] - dPrime[i - 1] * A[i]) / (B[i] - cPrime[i - 1] * A[i]);

        double[] k = new double[n];
        k[n - 1] = dPrime[n - 1];
        for (int i = n - 2; i >= 0; i--)
            k[i] = dPrime[i] - cPrime[i] * k[i + 1];

        for (int i = 1; i < n; i++)
        {
            dx1 = x[i] - x[i - 1];
            dy1 = y[i] - y[i - 1];
            a[i - 1] = k[i - 1] * dx1 - dy1;
            b[i - 1] = -k[i] * dx1 + dy1;
        }

        return (a, b);
    }
}
```

### Program.cs

This is the source code I used to generate the figures on this page. 

Plots were generated using [ScottPlot.NET](https://scottplot.net).

```cs
// this function represents the signal being measured
static double f(double x) => Math.Sin(x * 10) + Math.Sin(x * 13f);

// create points representing randomly sampled time points of a smooth curve
Random rand = new(123);
double[] sampleXs = Enumerable.Range(0, 20)
    .Select(x => rand.NextDouble())
    .OrderBy(x => x)
    .ToArray();
double[] sampleYs = sampleXs.Select(x => f(x)).ToArray();

// use 1D interpolation to create an evenly sampled curve from unevenly sampled data
(double[] xs, double[] ys) = Interpolation.Interpolate1D(sampleXs, sampleYs, count: 50);

var plt = new ScottPlot.Plot(600, 400);

double[] theoreticalXs = ScottPlot.DataGen.Range(xs.Min(), xs.Max(), .01);
double[] theoreticalYs = theoreticalXs.Select(x => f(x)).ToArray();
var perfectPlot = plt.AddScatterLines(theoreticalXs, theoreticalYs);
perfectPlot.Label = "theoretical signal";
perfectPlot.Color = plt.Palette.GetColor(2);
perfectPlot.LineStyle = ScottPlot.LineStyle.Dash;

var samplePlot = plt.AddScatterPoints(sampleXs, sampleYs);
samplePlot.Label = "sampled points";
samplePlot.Color = plt.Palette.GetColor(0);
samplePlot.MarkerSize = 10;
samplePlot.MarkerShape = ScottPlot.MarkerShape.openCircle;
samplePlot.MarkerLineWidth = 2;

var smoothPlot = plt.AddScatter(xs, ys);
smoothPlot.Label = "interpolated points";
smoothPlot.Color = plt.Palette.GetColor(3);
smoothPlot.MarkerShape = ScottPlot.MarkerShape.filledCircle;

plt.Legend();
plt.SaveFig("output.png");
```

## 2D and 3D Spline Interpolation

**The interpolation method described above only considered the horizontal axis** when generating evenly-spaced time points (1D interpolation). For information and code examples regarding 2D and 3D cubic spline interpolation, see my previous blog post: [Spline Interpolation with C#](https://swharden.com/blog/2022-01-22-spline-interpolation/) 

<a href="https://swharden.com/blog/2022-01-22-spline-interpolation/"><img src="https://swharden.com/blog/2022-01-22-spline-interpolation/screenshot.gif" class="mx-auto d-block mb-5"></a>

## Resources

* [2D and 3D Spline Interpolation with C#](https://swharden.com/blog/2022-01-22-spline-interpolation/) - Blog post from Jan, 2022

* [Spline Interpolation with ScottPlot](https://scottplot.net/cookbook/4.1/category/misc/#spline-interpolation) - Demonstrates additional types of interpolation: Bezier, Catmull-Rom, Chaikin, Cubic, etc. The project is open source under a MIT license.

* [Programmer's guide to polynomials and splines](https://wordsandbuttons.online/programmers_guide_to_polynomials_and_splines.html)
May 25th, 2022

Use Maui.Graphics to Draw 2D Graphics in Any .NET Application

This week Microsoft officially released .NET Maui and the new Microsoft.Maui.Graphics library which can draw 2D graphics in any .NET application (not just Maui apps). This page offers a quick look at how to use this new library to draw graphics using SkiaSharp in a .NET 6 console application. The C# Data Visualization site has additional examples for drawing and animating graphics using Microsoft.Maui.Graphics in Windows Forms and WPF applications.

The code below is a full .NET 6 console application demonstrating common graphics tasks (setting colors, drawing shapes, rendering text, etc.) and was used to generate the image above.

// These packages are available on NuGet
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

// Create a bitmap in memory and draw on its Canvas
SkiaBitmapExportContext bmp = new(600, 400, 1.0f);
ICanvas canvas = bmp.Canvas;

// Draw a big blue rectangle with a dark border
Rect backgroundRectangle = new(0, 0, bmp.Width, bmp.Height);
canvas.FillColor = Color.FromArgb("#003366");
canvas.FillRectangle(backgroundRectangle);
canvas.StrokeColor = Colors.Black;
canvas.StrokeSize = 20;
canvas.DrawRectangle(backgroundRectangle);

// Draw circles randomly around the image
for (int i = 0; i < 100; i++)
{
    float x = Random.Shared.Next(bmp.Width);
    float y = Random.Shared.Next(bmp.Height);
    float r = Random.Shared.Next(5, 50);

    Color randomColor = Color.FromRgb(
        red: Random.Shared.Next(255),
        green: Random.Shared.Next(255),
        blue: Random.Shared.Next(255));

    canvas.StrokeSize = r / 3;
    canvas.StrokeColor = randomColor.WithAlpha(.3f);
    canvas.DrawCircle(x, y, r);
}

// Measure a string
string myText = "Hello, Maui.Graphics!";
Font myFont = new Font("Impact");
float myFontSize = 48;
canvas.Font = myFont;
SizeF textSize = canvas.GetStringSize(myText, myFont, myFontSize);

// Draw a rectangle to hold the string
Point point = new(
    x: (bmp.Width - textSize.Width) / 2,
    y: (bmp.Height - textSize.Height) / 2);
Rect myTextRectangle = new(point, textSize);
canvas.FillColor = Colors.Black.WithAlpha(.5f);
canvas.FillRectangle(myTextRectangle);
canvas.StrokeSize = 2;
canvas.StrokeColor = Colors.Yellow;
canvas.DrawRectangle(myTextRectangle);

// Daw the string itself
canvas.FontSize = myFontSize * .9f; // smaller than the rectangle
canvas.FontColor = Colors.White;
canvas.DrawString(myText, myTextRectangle, 
    HorizontalAlignment.Center, VerticalAlignment.Center, TextFlow.OverflowBounds);

// Save the image as a PNG file
bmp.WriteToFile("console2.png");

Multi-Platform Graphics Abstraction

The Microsoft.Maui.Graphics namespace a small collection of interfaces which can be implemented by many different rendering technologies (SkiaSharp, SharpDX, GDI, etc.), making it possible to create drawing routines that are totally abstracted from the underlying graphics rendering system.

I really like that I can now create a .NET Standard 2.0 project that exclusively uses interfaces from Microsoft.Maui.Graphics to write code that draws complex graphics, then reference that code from other projects that use platform-specific graphics libraries to render the images.

When I write scientific simulations or data visualization code I frequently regard my graphics drawing routines as business logic, and drawing with Maui.Graphics lets me write this code to an abstraction that keeps rendering technology dependencies out of my business logic - a big win!

Rough Edges

After working with this library while it was being developed over the last few months, these are the things I find most limiting in my personal projects which made it through the initial release this week. Some of them are open issues so they may get fixed soon, and depending on how the project continues to evolve many of these rough edges may improve with time. I'm listing them here now so I can keep track of them, and I intend to update this list if/as these topics improve:

  • Strings cannot be accurately measured: The size returned by GetStringSize() is inaccurate and does not respect font. There's an issue tracking this (#279), but it's been open for more than three months and the library was released this week in its broken state.

    • EDIT: I concede multi-platform font support is a very hard problem, but this exactly the type of problem that .NET Maui was created to solve.

  • Missing XML documentation: Intellisense can really help people who are new to a library. The roll-out of a whole new application framework is a good example of a time when a lot of people will be exploring a new library. Let's take the Color class for example (which 100% of people will interact with) and consider misunderstandings that could be prevented by XML documentation and intellisense: If new Color() accepts 3 floats, should they be 0-255 or 0-1? I need to make a color from the RGB web value #003366, why does Color.FromHex() tell me to use FromArgb? Web colors are RGBA, should I use FromRrgba()? But wait, that string is RGB, not ARGB or RGBA, so will it throw an exception? What does Color.Parse() do?

    • Edit 1: Some of these answers are documented in source code, but they are not XML docs, so this information is not available to library users.

    • Edit 2: Is it on the open-source community to contribute XML documentation? If so, fair enough, but it is a very extensive effort (to write and to review), so a call should be put out for this job to ensure someone doesn't go through all the effort then have their open PR sit unmerged for months while it falls out of sync with the main branch.

  • The library has signs of being incomplete: There remain a good number of NotImplementedException and // todo in sections of the code base that indicate additional work is still required.

Again, I'm pointing these things out the very first week .NET Maui was released, so there's plenty of time and opportunity for improvements in the coming weeks and months.

I'm optimistic this library will continue to improve, and I am very excited to watch it progress! I'm not aware of the internal pressures and constraints that led to the library being released like it was this week, but I want to end by complimenting the team on their great job so far and encourage everyone (at Microsoft and in the open-source community at large) to keep moving this library forward. The .NET Maui team undertook an ambitious challenge by setting-out to implement cross-platform graphics support, but achieving this goal elegantly will be a huge accomplishment for the .NET community!

Resources

Markdown source code last modified on May 26th, 2022
---
title: Use Maui.Graphics to Draw 2D Graphics in Any .NET Application
description: How to use Microsoft.Maui.Graphics to draw graphics in a .NET console application and save the output as an image file using SkiaSharp
date: 2022-05-25 22:13:00
tags: csharp, graphics, maui
---

# Use Maui.Graphics to Draw 2D Graphics in Any .NET Application

**This week Microsoft officially released .NET Maui and the new `Microsoft.Maui.Graphics` library which can draw 2D graphics in any .NET application (not just Maui apps).** This page offers a quick look at how to use this new library to draw graphics using SkiaSharp in a .NET 6 console application. The [C# Data Visualization](https://swharden.com/csdv/) site has additional examples for drawing and animating graphics using `Microsoft.Maui.Graphics` in Windows Forms and WPF applications.

<img src="maui-graphics-quickstart.png" class="mx-auto my-5 d-block shadow">

The code below is a full .NET 6 console application demonstrating common graphics tasks (setting colors, drawing shapes, rendering text, etc.) and was used to generate the image above.

```cs
// These packages are available on NuGet
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

// Create a bitmap in memory and draw on its Canvas
SkiaBitmapExportContext bmp = new(600, 400, 1.0f);
ICanvas canvas = bmp.Canvas;

// Draw a big blue rectangle with a dark border
Rect backgroundRectangle = new(0, 0, bmp.Width, bmp.Height);
canvas.FillColor = Color.FromArgb("#003366");
canvas.FillRectangle(backgroundRectangle);
canvas.StrokeColor = Colors.Black;
canvas.StrokeSize = 20;
canvas.DrawRectangle(backgroundRectangle);

// Draw circles randomly around the image
for (int i = 0; i < 100; i++)
{
    float x = Random.Shared.Next(bmp.Width);
    float y = Random.Shared.Next(bmp.Height);
    float r = Random.Shared.Next(5, 50);

    Color randomColor = Color.FromRgb(
        red: Random.Shared.Next(255),
        green: Random.Shared.Next(255),
        blue: Random.Shared.Next(255));

    canvas.StrokeSize = r / 3;
    canvas.StrokeColor = randomColor.WithAlpha(.3f);
    canvas.DrawCircle(x, y, r);
}

// Measure a string
string myText = "Hello, Maui.Graphics!";
Font myFont = new Font("Impact");
float myFontSize = 48;
canvas.Font = myFont;
SizeF textSize = canvas.GetStringSize(myText, myFont, myFontSize);

// Draw a rectangle to hold the string
Point point = new(
    x: (bmp.Width - textSize.Width) / 2,
    y: (bmp.Height - textSize.Height) / 2);
Rect myTextRectangle = new(point, textSize);
canvas.FillColor = Colors.Black.WithAlpha(.5f);
canvas.FillRectangle(myTextRectangle);
canvas.StrokeSize = 2;
canvas.StrokeColor = Colors.Yellow;
canvas.DrawRectangle(myTextRectangle);

// Daw the string itself
canvas.FontSize = myFontSize * .9f; // smaller than the rectangle
canvas.FontColor = Colors.White;
canvas.DrawString(myText, myTextRectangle, 
    HorizontalAlignment.Center, VerticalAlignment.Center, TextFlow.OverflowBounds);

// Save the image as a PNG file
bmp.WriteToFile("console2.png");
```

## Multi-Platform Graphics Abstraction

**The `Microsoft.Maui.Graphics` namespace a small collection of interfaces which can be implemented by many different rendering technologies** (SkiaSharp, SharpDX, GDI, etc.), making it possible to create drawing routines that are totally abstracted from the underlying graphics rendering system.

I really like that I can now create a .NET Standard 2.0 project that exclusively uses interfaces from `Microsoft.Maui.Graphics` to write code that draws complex graphics, then reference that code from other projects that use platform-specific graphics libraries to render the images.

When I write scientific simulations or data visualization code I frequently regard my graphics drawing routines as business logic, and drawing with Maui.Graphics lets me write this code to an abstraction that keeps rendering technology dependencies out of my business logic - a big win!

## Rough Edges

After working with this library while it was being developed over the last few months, these are the things I find most limiting in my personal projects which made it through the initial release this week. Some of them are [open issues](https://github.com/dotnet/Microsoft.Maui.Graphics/issues) so they may get fixed soon, and depending on how the project continues to evolve many of these rough edges may improve with time. I'm listing them here now so I can keep track of them, and I intend to update this list if/as these topics improve:

<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="check-circle-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
  </symbol>
  <symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
  </symbol>
  <symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
  </symbol>
</svg>

<div class="alert alert-primary d-flex align-items-center" role="alert">
  <svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
  <div>
    <strong>Note:</strong> This section was last reviewed on April 25, 2022 and improvements may have been made since this text was written.
  </div>
</div>

* **Strings cannot be accurately measured:** The size returned by `GetStringSize()` is inaccurate and does not respect font. There's an issue tracking this ([#279](https://github.com/dotnet/Microsoft.Maui.Graphics/issues/279)), but it's been open for more than three months and the library was released this week in its broken state.

  * EDIT: I concede multi-platform font support is a very hard problem, but this exactly the type of problem that .NET Maui was created to solve.<br><br>

* **Missing XML documentation:** Intellisense can really help people who are new to a library. The roll-out of a whole new application framework is a good example of a time when a lot of people will be exploring a new library. Let's take the [`Color` class](https://github.com/dotnet/Microsoft.Maui.Graphics/blob/main/src/Microsoft.Maui.Graphics/Color.cs) for example (which 100% of people will interact with) and consider misunderstandings that could be prevented by XML documentation and intellisense: If `new Color()` accepts 3 floats, should they be 0-255 or 0-1? I need to make a color from the RGB web value `#003366`, why does `Color.FromHex()` tell me to use `FromArgb`? Web colors are RGBA, should I use `FromRrgba()`? But wait, that string is RGB, not ARGB or RGBA, so will it throw an exception? What does `Color.Parse()` do?

  * Edit 1: Some of these answers are [documented in source code](https://github.com/dotnet/Microsoft.Maui.Graphics/blob/e15f2d552d851c28771e7fe092895e908395f8a4/src/Microsoft.Maui.Graphics/Color.cs#L574-L590), but they are not XML docs, so this information is not available to library users.

  * Edit 2: Is it on the open-source community to contribute XML documentation? If so, fair enough, but it is a very extensive effort (to write _and_ to review), so a call should be put out for this job to ensure someone doesn't go through all the effort then have their open PR sit unmerged for months while it falls out of sync with the main branch.

* **The library has signs of being incomplete:** There remain a good number of [NotImplementedException](https://github.com/dotnet/Microsoft.Maui.Graphics/search?q=NotImplementedException) and [// todo](https://github.com/dotnet/Microsoft.Maui.Graphics/search?q=todo) in sections of the code base that indicate additional work is still required.

Again, I'm pointing these things out the very first week .NET Maui was released, so there's plenty of time and opportunity for improvements in the coming weeks and months.

**I'm optimistic this library will continue to improve, and I am very excited to watch it progress!** I'm not aware of the internal pressures and constraints that led to the library being released like it was this week, but I want to end by complimenting the team on their great job so far and encourage everyone (at Microsoft and in the open-source community at large) to keep moving this library forward. The .NET Maui team undertook an ambitious challenge by setting-out to implement cross-platform graphics support, but achieving this goal elegantly will be a huge accomplishment for the .NET community!

## Resources
* [Source code for this project](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/maui-graphics)
* [Maui.Graphics WinForms Quickstart](https://swharden.com/csdv/maui.graphics/quickstart-winforms/)
* [Maui.Graphics WPF Quickstart](https://swharden.com/csdv/maui.graphics/quickstart-wpf/)
* [Maui.Graphics Console Quickstart](https://swharden.com/csdv/maui.graphics/quickstart-console/)
* [Maui.Graphics .NET Maui Quickstart](https://swharden.com/csdv/maui.graphics/quickstart-maui/)
* [https://maui.graphics](https://maui.graphics)
April 4th, 2022

Mystify your Mind with SkiaSharp

This article explores my recreation of the classic screensaver Mystify your Mind implemented using C#. I used SkiaSharp to draw graphics and FFMpegCore to encode frames into high definition video files suitable for YouTube.

The Mystify Sandbox application has advanced options allowing exploration of various configurations outside the capabilities of the original screensaver. Interesting configurations can be exported as video (x264-encoded MP4 or WebM format) or viewed in full-screen mode resembling an actual screensaver.

Download

Programming Strategy

  • Corner - tracks point that bounces around the edges of the screen
    • Has Position and Velocity fields
    • Has Advance() to move points collide with edges
  • Wire - represents a single polygon that moves around the screen
    • Contains List<Corner> and a Color which all change over time
    • Has Advance() which advances all corner and cycles Color.
    • Contains List<WireSnapshot> to record history
  • WireSnapshot - represents properties of a Wire at an instant in time
    • Contains Point[] and Color and is intended to be immutable
    • Can draw itself using a Draw() method that accepts a SKCanvas
  • Field - represents the whole animation
    • Contains List<Wire> and has Width and Height
    • Has Advance() which advances all wires
    • Can draw itself using a Draw() method that accepts a SKCanvas

Original Behavior

Close inspection of video from the original Mystify screensaver revealed notable behaviors.

Broken Lines

The original Mystify implementation did not clear the screen and between every frame. With GDI large fills (clearing the background) are expensive, and drawing many polygons probably challenged performance in the 90s. Instead only the leading wire was drawn, and the trailing wire was drawn-over using black. This strategy results in lines which appear to have single pixel breaks on a black background (magenta arrow). It may not have been particularly visible on CRT monitors available in the 90s, but it is quite noticeable on LCD screens today.

Bouncing Changes Speed

Observing videos of the classic screensaver I noticed that corners don't bounce symmetrically off edges. After every bounce they change their speed slightly. This can be seen by observing the history of corners which reflect off edges of the screen demonstrating their change in speed (green arrow). I recreated this behavior using a weighted random number generator.

Programming Notes

Color Cycling

I used a HSL-to-RGB method to generate colors from hue (variable), saturation (always 100%), and luminosity (always 50%). By repeatedly ramping hue from 0% to 100% slowly I achieved a rainbow gradient effect. Increasing the color change speed (% change for every new wire) cycles the colors faster, and very high values produce polygons whose visible history spans a gradient of colors. Fade effect is achieved by increasing alpha of wire snapshots as they are drawn from old to new.

Encoding video with C

The FFMpegCore package is a C# wrapper for FFMpeg that can encode video from frames piped into it. Using this strategy required creation of a SkiaSharp.SKBitmap wrapper that implements FFMpegCore.Pipes.IVideoFrame. For a full explaination and example code see C# Data Visualization: Render Video with SkiaSharp.

Performance

It's amusing to see retro screensavers running on modern gear! I can run this graphics model simulation at full-screen resolutions using thousands of wires at real-time frame rates. The most natural density of shapes for my 3440x1440 display was 20 wires with a history of 5.

Rendering the 2D image and encoding HD video using the x264 codec occupies all my CPU cores and runs a little above 500 frames per second. Encoding 24 hours of video (over 2 million frames) took this system 1 hour and 12 minutes and produced a 15.3 GB MP4 file. Encoding WebM format is considerably slower, with the same system only achieving an encoding rate of 12 frames per second.

Simulations

Traditional Behavior

The classic screensaver is typically run with two 4-cornered polygons that slowly change color.

Rainbow

Increasing the rate of color transition produces a rainbow effect within the visible history of polygons. The effect is made more striking by increasing the history length and decreasing the speed so the historical lines are closer together.

Solid

If the speed is greatly decreased and the number of historical records is greatly increased the resulting shape has little or no gap between historical traces and appears like a solid object. If fading is enabled (where opacity of older traces fades to transparent) the resulting effect is very interesting.

Chaos

Adding 100 shapes produces a chaotic but interesting effect. This may be the first time the world has seen Mystify like this!

EDIT: All these lines are very stressful on the video encoder and produce large file sizes to achieve high quality (25 MB for 10 seconds). I'm showing this one as a JPEG but click here to view mystify-100.webm if you're on a good internet connection.

YouTube

Resources

Markdown source code last modified on April 9th, 2022
---
title: Mystify your Mind with SkiaSharp
description: My implementation of the classic screensaver using SkiaSharp, OpenGL, and FFMpeg
date: 2022-04-04 18:34:00
tags: csharp, graphics
---

# Mystify your Mind with SkiaSharp

**This article explores my recreation of the classic screensaver _Mystify your Mind_ implemented using C#.** I used [SkiaSharp](https://github.com/mono/SkiaSharp) to draw graphics and [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) to encode frames into high definition video files suitable for YouTube.

<div class="text-center">

![](mystify.gif)

</div>

**The Mystify Sandbox application has advanced options** allowing exploration of various configurations outside the capabilities of the original screensaver. Interesting configurations can be exported as video (x264-encoded MP4 or WebM format) or viewed in full-screen mode resembling an actual screensaver. 

![](mystify-advanced.jpg)

## Download
* The [Releases page](https://github.com/swharden/Mystify/releases) has a click-to-run EXE for Windows
* [GitHub.com/swharden/Mystify](https://github.com/swharden/Mystify/) contains project source code (C#/.NET6)

## Programming Strategy

* `Corner` - tracks point that bounces around the edges of the screen
  * Has `Position` and `Velocity` fields
  * Has `Advance()` to move points collide with edges
* `Wire` - represents a single polygon that moves around the screen
  * Contains `List<Corner>` and a `Color` which all change over time
  * Has `Advance()` which advances all corner and cycles `Color`.
  * Contains `List<WireSnapshot>` to record history
* `WireSnapshot` - represents properties of a `Wire` at an instant in time
  * Contains `Point[]` and `Color` and is intended to be immutable
  * Can draw itself using a `Draw()` method that accepts a `SKCanvas`
* `Field` - represents the whole animation
  * Contains `List<Wire>` and has `Width` and `Height`
  * Has `Advance()` which advances all wires
  * Can draw itself using a `Draw()` method that accepts a `SKCanvas`

## Original Behavior

Close inspection of [video from the original](https://youtu.be/SaBvcHHdlGE) Mystify screensaver revealed notable behaviors.

<img src="mystify-inspection.jpg" class="d-block shadow mx-auto my-5">

### Broken Lines
The original Mystify implementation did not clear the screen and between every frame. With GDI large fills (clearing the background) are expensive, and drawing many polygons probably challenged performance in the 90s. Instead only the leading wire was drawn, and the trailing wire was drawn-over using black. This strategy results in lines which appear to have single pixel breaks on a black background (magenta arrow). It may not have been particularly visible on CRT monitors available in the 90s, but it is quite noticeable on LCD screens today.

### Bouncing Changes Speed
Observing videos of the classic screensaver I noticed that corners don't bounce symmetrically off edges. After every bounce they change their speed slightly. This can be seen by observing the history of corners which reflect off edges of the screen demonstrating their change in speed (green arrow). I recreated this behavior using a weighted random number generator.

## Programming Notes

### Color Cycling
I used a HSL-to-RGB method to generate colors from hue (variable), saturation (always 100%), and luminosity (always 50%). By repeatedly ramping hue from 0% to 100% slowly I achieved a rainbow gradient effect. Increasing the color change speed (% change for every new wire) cycles the colors faster, and very high values produce polygons whose visible history spans a gradient of colors. Fade effect is achieved by increasing alpha of wire snapshots as they are drawn from old to new.

### Encoding video with C#
The FFMpegCore package is a C# wrapper for FFMpeg that can encode video from frames piped into it. Using this strategy required creation of a `SkiaSharp.SKBitmap` wrapper that implements `FFMpegCore.Pipes.IVideoFrame`. For a full explaination and example code see [C# Data Visualization: Render Video with SkiaSharp](https://swharden.com/csdv/skiasharp/video/).

### Performance

**It's amusing to see retro screensavers running on modern gear!** I can run this graphics model simulation at full-screen resolutions using thousands of wires at real-time frame rates. The most natural density of shapes for my 3440x1440 display was 20 wires with a history of 5.

<img src="desk.jpg" class="d-block shadow mx-auto my-5">

Rendering the 2D image and encoding HD video using the x264 codec occupies all my CPU cores and runs a little above 500 frames per second. Encoding 24 hours of video (over 2 million frames) took this system 1 hour and 12 minutes and produced a 15.3 GB MP4 file. Encoding WebM format is considerably slower, with the same system only achieving an encoding rate of 12 frames per second.

<img src="cpu.png" class="d-block mx-auto my-5">


## Simulations

### Traditional Behavior

The classic screensaver is typically run with two 4-cornered polygons that slowly change color.

<video width="759" height="470" controls class="d-block mx-auto my-5 shadow" style="max-width: 100%; height: 100%;">
  <source src="mystify-01-standard.webm" type="video/mp4">
</video>

### Rainbow

Increasing the rate of color transition produces a rainbow effect within the visible history of polygons. The effect is made more striking by increasing the history length and decreasing the speed so the historical lines are closer together.

<video width="759" height="470" controls class="d-block mx-auto my-5 shadow" style="max-width: 100%; height: 100%;">
  <source src="mystify-02-rainbow.webm" type="video/mp4">
</video>

### Solid

If the speed is greatly decreased and the number of historical records is greatly increased the resulting shape has little or no gap between historical traces and appears like a solid object. If fading is enabled (where opacity of older traces fades to transparent) the resulting effect is very interesting.

<video width="759" height="470" controls class="d-block mx-auto my-5 shadow" style="max-width: 100%; height: 100%;">
  <source src="mystify-03-solid.webm" type="video/mp4">
</video>

### Chaos

Adding 100 shapes produces a chaotic but interesting effect. This may be the first time the world has seen Mystify like this!

_EDIT: All these lines are very stressful on the video encoder and produce large file sizes to achieve high quality (25 MB for 10 seconds). I'm showing this one as a JPEG but [click here to view mystify-100.webm](mystify-04-100.webm) if you're on a good internet connection._

<a href='mystify-04-100.webm'><img src="mystify-04-100.jpg" class="d-block mx-auto my-5 shadow"></a>

## YouTube

<div class="text-center">

![](https://youtu.be/queN9r3Leis)

</div>

## Resources
* A click-to-run EXE can be downloaded from the [Releases Page](https://github.com/swharden/Mystify/releases)
* Source Code is available on https://github.com/swharden/Mystify
* Implementation Details: [C# Data Visualization: Mystify](https://swharden.com/csdv/simulations/mystify/)
* [C# Data Visualization: Render Video with SkiaSharp](https://swharden.com/csdv/skiasharp/video/)
* GitHub: [SkiaSharp](https://github.com/mono/SkiaSharp)
* GitHub: [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) 
* Windows 3.1 Mystify (video): https://youtu.be/osCZyfoScFg?t=370
* Windows 95 Mystify (video): https://youtu.be/SaBvcHHdlGE
Pages