diff --git a/README.md b/README.md
index 62790c8..9b964ce 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,17 @@
_[](https://www.nuget.org/packages/spectre.console)_
-A .NET Standard 2.0 library that makes it easier to create beautiful console applications.
+A .NET 5/.NET Standard 2.0 library that makes it easier to create beautiful, cross platform, console applications.
It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich)
for Python.
## Table of Contents
1. [Features](#features)
-2. [Example](#example)
-3. [Installing](#installing)
-4. [Usage](#usage)
- 4.1. [Using the static API](#using-the-static-api)
- 4.2. [Creating a console](#creating-a-console)
-5. [Running examples](#running-examples)
+2. [Installing](#installing)
+3. [Documentation](#documentation)
+4. [Examples](#examples)
+5. [License](#license)
## Features
@@ -25,77 +23,27 @@ for Python.
and blinking text.
* Supports 3/4/8/24-bit colors in the terminal.
The library will detect the capabilities of the current terminal
- and downgrade colors as needed.
+ and downgrade colors as needed.
-## Example

## Installing
-The fastest way of getting started using Spectre.Console is to install the NuGet package.
+The fastest way of getting started using `Spectre.Console` is to install the NuGet package.
```csharp
dotnet add package Spectre.Console
```
-## Usage
+## Documentation
-The `Spectre.Console` API is stateful and is not thread-safe.
-If you need to write to the console from different threads, make sure that
-you take appropriate precautions, just like when you use the
-regular `System.Console` API.
+The documentation for `Spectre.Console` can be found at
+https://spectresystems.github.io/spectre.console/
-If the current terminal does not support ANSI escape sequences,
-`Spectre.Console` will fallback to using the `System.Console` API.
+## Examples
-_NOTE: This library is currently under development and APIs
-might change or get removed at any point up until a 1.0 release._
-
-### Using the static API
-
-The static API is perfect when you just want to output text
-like you usually do with the `System.Console` API, but prettier.
-
-```csharp
-AnsiConsole.Foreground = Color.CornflowerBlue;
-AnsiConsole.Decoration = Decoration.Underline | Decoration.Bold;
-AnsiConsole.WriteLine("Hello World!");
-
-AnsiConsole.Reset();
-AnsiConsole.MarkupLine("[bold yellow on red]{0}[/] [underline]world[/]!", "Goodbye");
-```
-
-If you want to get a reference to the default `IAnsiConsole`,
-you can access it via `AnsiConsole.Console`.
-
-### Creating a console
-
-Sometimes it's useful to explicitly create a console with specific
-capabilities, such as during unit testing when you want control
-over the environment your code runs in.
-
-It's recommended to not use `AnsiConsole` in code that run as
-part of a unit test.
-
-```csharp
-IAnsiConsole console = AnsiConsole.Create(
- new AnsiConsoleSettings()
- {
- Ansi = AnsiSupport.Yes,
- ColorSystem = ColorSystemSupport.TrueColor,
- Out = new StringWriter(),
- });
-```
-
-_NOTE: Even if you can specify a specific color system to use
-when manually creating a console, remember that the user's terminal
-might not be able to use it, so unless you're creating an IAnsiConsole
-for testing, always use `ColorSystemSupport.Detect` and `AnsiSupport.Detect`._
-
-## Running examples
-
-To see Spectre.Console in action, install the
+To see `Spectre.Console` in action, install the
[dotnet-example](https://github.com/patriksvensson/dotnet-example)
global tool.
@@ -107,34 +55,18 @@ Now you can list available examples in this repository:
```
> dotnet example
-
-╭────────────┬───────────────────────────────────────┬──────────────────────────────────────────────────────╮
-│ Name │ Path │ Description │
-├────────────┼───────────────────────────────────────┼──────────────────────────────────────────────────────┤
-│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │
-│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │
-│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │
-│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │
-│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │
-│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │
-│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │
-│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │
-│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │
-│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │
-│ Rules │ examples/Rules/Rules.csproj │ Demonstrates how to render horizontal rules (lines). │
-│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │
-╰────────────┴───────────────────────────────────────┴──────────────────────────────────────────────────────╯
```
And to run an example:
```
> dotnet example tables
-┌──────────┬──────────┬────────┐
-│ Foo │ Bar │ Baz │
-├──────────┼──────────┼────────┤
-│ Hello │ World! │ │
-│ Bonjour │ le │ monde! │
-│ Hej │ Världen! │ │
-└──────────┴──────────┴────────┘
-```
\ No newline at end of file
+```
+
+## License
+
+Copyright © Spectre Systems.
+
+Spectre.Console is provided as-is under the MIT license. For more information see LICENSE.
+
+* For SixLabors.ImageSharp, see https://github.com/SixLabors/ImageSharp/blob/master/LICENSE
\ No newline at end of file
diff --git a/examples/Canvas/Canvas.csproj b/examples/Canvas/Canvas.csproj
new file mode 100644
index 0000000..a98bd15
--- /dev/null
+++ b/examples/Canvas/Canvas.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ false
+ Canvas
+ Demonstrates how to render pixels and images.
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/examples/Canvas/Mandelbrot.cs b/examples/Canvas/Mandelbrot.cs
new file mode 100644
index 0000000..7f3a3da
--- /dev/null
+++ b/examples/Canvas/Mandelbrot.cs
@@ -0,0 +1,87 @@
+/*
+Ported from: https://rosettacode.org/wiki/Mandelbrot_set#C.23
+Licensed under GNU Free Documentation License 1.2
+*/
+
+using System;
+using Spectre.Console;
+
+namespace CanvasExample
+{
+ public static class Mandelbrot
+ {
+ private const double MaxValueExtent = 2.0;
+
+ private struct ComplexNumber
+ {
+ public double Real { get; }
+ public double Imaginary { get; }
+
+ public ComplexNumber(double real, double imaginary)
+ {
+ Real = real;
+ Imaginary = imaginary;
+ }
+
+ public static ComplexNumber operator +(ComplexNumber x, ComplexNumber y)
+ {
+ return new ComplexNumber(x.Real + y.Real, x.Imaginary + y.Imaginary);
+ }
+
+ public static ComplexNumber operator *(ComplexNumber x, ComplexNumber y)
+ {
+ return new ComplexNumber(x.Real * y.Real - x.Imaginary * y.Imaginary,
+ x.Real * y.Imaginary + x.Imaginary * y.Real);
+ }
+
+ public double Abs()
+ {
+ return Real * Real + Imaginary * Imaginary;
+ }
+ }
+
+ public static Canvas Generate(int width, int height)
+ {
+ var canvas = new Canvas(width, height);
+
+ var scale = 2 * MaxValueExtent / Math.Min(canvas.Width, canvas.Height);
+ for (var i = 0; i < canvas.Height; i++)
+ {
+ var y = (canvas.Height / 2 - i) * scale;
+ for (var j = 0; j < canvas.Width; j++)
+ {
+ var x = (j - canvas.Width / 2) * scale;
+ var value = Calculate(new ComplexNumber(x, y));
+ canvas.SetPixel(j, i, GetColor(value));
+ }
+ }
+
+ return canvas;
+ }
+
+ private static double Calculate(ComplexNumber c)
+ {
+ const int MaxIterations = 1000;
+ const double MaxNorm = MaxValueExtent * MaxValueExtent;
+
+ var iteration = 0;
+ var z = new ComplexNumber();
+ do
+ {
+ z = z * z + c;
+ iteration++;
+ } while (z.Abs() < MaxNorm && iteration < MaxIterations);
+
+ return iteration < MaxIterations
+ ? (double)iteration / MaxIterations
+ : 0;
+ }
+
+ private static Color GetColor(double value)
+ {
+ const double MaxColor = 256;
+ const double ContrastValue = 0.2;
+ return new Color(0, 0, (byte)(MaxColor * Math.Pow(value, ContrastValue)));
+ }
+ }
+}
diff --git a/examples/Canvas/Program.cs b/examples/Canvas/Program.cs
new file mode 100644
index 0000000..337961d
--- /dev/null
+++ b/examples/Canvas/Program.cs
@@ -0,0 +1,36 @@
+using SixLabors.ImageSharp.Processing;
+using Spectre.Console;
+using Spectre.Console.Rendering;
+
+namespace CanvasExample
+{
+ public static class Program
+ {
+ public static void Main()
+ {
+ // Draw a mandelbrot set using a Canvas
+ var mandelbrot = Mandelbrot.Generate(32, 32);
+ Render(mandelbrot, "Mandelbrot");
+
+ // Draw an image using CanvasImage powered by ImageSharp.
+ // This requires the "Spectre.Console.ImageSharp" NuGet package.
+ var image = new CanvasImage("cake.png");
+ image.BilinearResampler();
+ image.MaxWidth(16);
+ Render(image, "Image from file (16 wide)");
+
+ // Draw image again, but without render width
+ image.NoMaxWidth();
+ image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop());
+ Render(image, "Image from file (fit, greyscale, rotated)");
+ }
+
+ private static void Render(IRenderable canvas, string title)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.Render(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey"));
+ AnsiConsole.WriteLine();
+ AnsiConsole.Render(canvas);
+ }
+ }
+}
diff --git a/examples/Canvas/cake.png b/examples/Canvas/cake.png
new file mode 100644
index 0000000..f11d285
Binary files /dev/null and b/examples/Canvas/cake.png differ
diff --git a/examples/Rules/Program.cs b/examples/Rules/Program.cs
index e65ac97..8a03924 100644
--- a/examples/Rules/Program.cs
+++ b/examples/Rules/Program.cs
@@ -7,34 +7,34 @@ namespace EmojiExample
public static void Main(string[] args)
{
// No title
- WrapInPanel(
+ Render(
new Rule()
.RuleStyle(Style.Parse("yellow"))
.AsciiBorder()
.LeftAligned());
// Left aligned title
- WrapInPanel(
+ Render(
new Rule("[blue]Left aligned[/]")
.RuleStyle(Style.Parse("red"))
.DoubleBorder()
.LeftAligned());
// Centered title
- WrapInPanel(
+ Render(
new Rule("[green]Centered[/]")
.RuleStyle(Style.Parse("green"))
.HeavyBorder()
.Centered());
// Right aligned title
- WrapInPanel(
+ Render(
new Rule("[red]Right aligned[/]")
.RuleStyle(Style.Parse("blue"))
.RightAligned());
}
- private static void WrapInPanel(Rule rule)
+ private static void Render(Rule rule)
{
AnsiConsole.Render(rule);
AnsiConsole.WriteLine();
diff --git a/src/Spectre.Console.ImageSharp/CanvasImage.cs b/src/Spectre.Console.ImageSharp/CanvasImage.cs
new file mode 100644
index 0000000..fdf4bea
--- /dev/null
+++ b/src/Spectre.Console.ImageSharp/CanvasImage.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents a renderable image.
+ ///
+ public sealed class CanvasImage : Renderable
+ {
+ private static readonly IResampler _defaultResampler = KnownResamplers.Bicubic;
+
+ ///
+ /// Gets the image width.
+ ///
+ public int Width => Image.Width;
+
+ ///
+ /// Gets the image height.
+ ///
+ public int Height => Image.Height;
+
+ ///
+ /// Gets or sets the render width of the canvas.
+ ///
+ public int? MaxWidth { get; set; }
+
+ ///
+ /// Gets or sets the render width of the canvas.
+ ///
+ public int PixelWidth { get; set; } = 2;
+
+ ///
+ /// Gets or sets the that should
+ /// be used when scaling the image. Defaults to bicubic sampling.
+ ///
+ public IResampler? Resampler { get; set; }
+
+ internal SixLabors.ImageSharp.Image Image { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The image filename.
+ public CanvasImage(string filename)
+ {
+ Image = SixLabors.ImageSharp.Image.Load(filename);
+ }
+
+ ///
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ if (PixelWidth < 0)
+ {
+ throw new InvalidOperationException("Pixel width must be greater than zero.");
+ }
+
+ var width = MaxWidth ?? Width;
+ if (maxWidth < width * PixelWidth)
+ {
+ return new Measurement(maxWidth, maxWidth);
+ }
+
+ return new Measurement(width * PixelWidth, width * PixelWidth);
+ }
+
+ ///
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ var image = Image;
+
+ var width = Width;
+ var height = Height;
+
+ // Got a max width?
+ if (MaxWidth != null)
+ {
+ height = (int)(height * ((float)MaxWidth.Value) / Width);
+ width = MaxWidth.Value;
+ }
+
+ // Exceed the max width when we take pixel width into account?
+ if (width * PixelWidth > maxWidth)
+ {
+ height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
+ width = maxWidth / PixelWidth;
+ }
+
+ // Need to rescale the pixel buffer?
+ if (width != Width || height != Height)
+ {
+ var resampler = Resampler ?? _defaultResampler;
+ image = image.Clone(); // Clone the original image
+ image.Mutate(i => i.Resize(width, height, resampler));
+ }
+
+ var canvas = new Canvas(width, height)
+ {
+ MaxWidth = MaxWidth,
+ PixelWidth = PixelWidth,
+ Scale = false,
+ };
+
+ for (var y = 0; y < image.Height; y++)
+ {
+ for (var x = 0; x < image.Width; x++)
+ {
+ if (image[x, y].A == 0)
+ {
+ continue;
+ }
+
+ canvas.SetPixel(x, y, new Color(
+ image[x, y].R, image[x, y].G, image[x, y].B));
+ }
+ }
+
+ return ((IRenderable)canvas).Render(context, maxWidth);
+ }
+ }
+}
diff --git a/src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs b/src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs
new file mode 100644
index 0000000..fa99fe6
--- /dev/null
+++ b/src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs
@@ -0,0 +1,135 @@
+using System;
+using SixLabors.ImageSharp.Processing;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class CanvasImageExtensions
+ {
+ ///
+ /// Sets the maximum width of the rendered image.
+ ///
+ /// The canvas image.
+ /// The maximum width.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage MaxWidth(this CanvasImage image, int? maxWidth)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ image.MaxWidth = maxWidth;
+ return image;
+ }
+
+ ///
+ /// Disables the maximum width of the rendered image.
+ ///
+ /// The canvas image.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage NoMaxWidth(this CanvasImage image)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ image.MaxWidth = null;
+ return image;
+ }
+
+ ///
+ /// Sets the pixel width.
+ ///
+ /// The canvas image.
+ /// The pixel width.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage PixelWidth(this CanvasImage image, int width)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ image.PixelWidth = width;
+ return image;
+ }
+
+ ///
+ /// Mutates the underlying image.
+ ///
+ /// The canvas image.
+ /// The action that mutates the underlying image.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage Mutate(this CanvasImage image, Action action)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ image.Image.Mutate(action);
+ return image;
+ }
+
+ ///
+ /// Uses a bicubic sampler that implements the bicubic kernel algorithm W(x).
+ ///
+ /// The canvas image.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage BicubicResampler(this CanvasImage image)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ image.Resampler = KnownResamplers.Bicubic;
+ return image;
+ }
+
+ ///
+ /// Uses a bilinear sampler. This interpolation algorithm
+ /// can be used where perfect image transformation with pixel matching is impossible,
+ /// so that one can calculate and assign appropriate intensity values to pixels.
+ ///
+ /// The canvas image.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage BilinearResampler(this CanvasImage image)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ image.Resampler = KnownResamplers.Triangle;
+ return image;
+ }
+
+ ///
+ /// Uses a Nearest-Neighbour sampler that implements the nearest neighbor algorithm.
+ /// This uses a very fast, unscaled filter which will select the closest pixel to
+ /// the new pixels position.
+ ///
+ /// The canvas image.
+ /// The same instance so that multiple calls can be chained.
+ public static CanvasImage NearestNeighborResampler(this CanvasImage image)
+ {
+ if (image is null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+
+ image.Resampler = KnownResamplers.NearestNeighbor;
+ return image;
+ }
+ }
+}
diff --git a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj
new file mode 100644
index 0000000..f5c18ad
--- /dev/null
+++ b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netstandard2.0
+ enable
+ A library that extends Spectre.Console with ImageSharp super powers.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln
index c9a4ba3..a1af2ed 100644
--- a/src/Spectre.Console.sln
+++ b/src/Spectre.Console.sln
@@ -54,6 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "..\examples\Promp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Canvas\Canvas.csproj", "{5693761A-754A-40A8-9144-36510D6A4D69}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -268,6 +272,30 @@ Global
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x64.Build.0 = Release|Any CPU
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.ActiveCfg = Release|Any CPU
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.Build.0 = Release|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x64.Build.0 = Debug|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x86.Build.0 = Debug|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x64.ActiveCfg = Release|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x64.Build.0 = Release|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x86.ActiveCfg = Release|Any CPU
+ {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x86.Build.0 = Release|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.Build.0 = Debug|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.Build.0 = Debug|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.ActiveCfg = Release|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.Build.0 = Release|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.ActiveCfg = Release|Any CPU
+ {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -289,6 +317,7 @@ Global
{75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
+ {5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
diff --git a/src/Spectre.Console/Widgets/Canvas.cs b/src/Spectre.Console/Widgets/Canvas.cs
new file mode 100644
index 0000000..acad4ce
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Canvas.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents a renderable canvas.
+ ///
+ public sealed class Canvas : Renderable
+ {
+ private readonly Color?[,] _pixels;
+
+ ///
+ /// Gets the width of the canvas.
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the height of the canvas.
+ ///
+ public int Height { get; }
+
+ ///
+ /// Gets or sets the render width of the canvas.
+ ///
+ public int? MaxWidth { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not
+ /// to scale the canvas when rendering.
+ ///
+ public bool Scale { get; set; } = true;
+
+ ///
+ /// Gets or sets the pixel width.
+ ///
+ public int PixelWidth { get; set; } = 2;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The canvas width.
+ /// The canvas height.
+ public Canvas(int width, int height)
+ {
+ Width = width;
+ Height = height;
+
+ _pixels = new Color?[Width, Height];
+ }
+
+ ///
+ /// Sets a pixel with the specified color in the canvas at the specified location.
+ ///
+ /// The X coordinate for the pixel.
+ /// The Y coordinate for the pixel.
+ /// The pixel color.
+ public void SetPixel(int x, int y, Color color)
+ {
+ _pixels[x, y] = color;
+ }
+
+ ///
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ if (PixelWidth < 0)
+ {
+ throw new InvalidOperationException("Pixel width must be greater than zero.");
+ }
+
+ var width = MaxWidth ?? Width;
+
+ if (maxWidth < width * PixelWidth)
+ {
+ return new Measurement(maxWidth, maxWidth);
+ }
+
+ return new Measurement(width * PixelWidth, width * PixelWidth);
+ }
+
+ ///
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ if (PixelWidth < 0)
+ {
+ throw new InvalidOperationException("Pixel width must be greater than zero.");
+ }
+
+ var pixels = _pixels;
+ var pixel = new string(' ', PixelWidth);
+ var width = Width;
+ var height = Height;
+
+ // Got a max width?
+ if (MaxWidth != null)
+ {
+ height = (int)(height * ((float)MaxWidth.Value) / Width);
+ width = MaxWidth.Value;
+ }
+
+ // Exceed the max width when we take pixel width into account?
+ if (width * PixelWidth > maxWidth)
+ {
+ height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
+ width = maxWidth / PixelWidth;
+ }
+
+ // Need to rescale the pixel buffer?
+ if (Scale && (width != Width || height != Height))
+ {
+ pixels = ScaleDown(width, height);
+ }
+
+ for (var y = 0; y < height; y++)
+ {
+ for (var x = 0; x < width; x++)
+ {
+ var color = pixels[x, y];
+ if (color != null)
+ {
+ yield return new Segment(pixel, new Style(background: color));
+ }
+ else
+ {
+ yield return new Segment(pixel);
+ }
+ }
+
+ yield return Segment.LineBreak;
+ }
+ }
+
+ private Color?[,] ScaleDown(int newWidth, int newHeight)
+ {
+ var buffer = new Color?[newWidth, newHeight];
+ var xRatio = ((Width << 16) / newWidth) + 1;
+ var yRatio = ((Height << 16) / newHeight) + 1;
+
+ for (var i = 0; i < newHeight; i++)
+ {
+ for (var j = 0; j < newWidth; j++)
+ {
+ buffer[j, i] = _pixels[(j * xRatio) >> 16, (i * yRatio) >> 16];
+ }
+ }
+
+ return buffer;
+ }
+ }
+}