Add canvas and image support

Adds support for drawing "pixels" and displaying
images in the terminal.
This commit is contained in:
Patrik Svensson 2020-11-24 21:24:21 +01:00 committed by Patrik Svensson
parent 4f6eca4fcb
commit 2a9fa223de
11 changed files with 633 additions and 94 deletions

110
README.md
View File

@ -2,19 +2,17 @@
_[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](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
![Example](resources/gfx/screenshots/example.png)
## 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! │ │
└──────────┴──────────┴────────┘
```
```
## 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

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Title>Canvas</Title>
<Description>Demonstrates how to render pixels and images.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj" />
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="cake.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -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)));
}
}
}

View File

@ -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);
}
}
}

BIN
examples/Canvas/cake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -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();

View File

@ -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
{
/// <summary>
/// Represents a renderable image.
/// </summary>
public sealed class CanvasImage : Renderable
{
private static readonly IResampler _defaultResampler = KnownResamplers.Bicubic;
/// <summary>
/// Gets the image width.
/// </summary>
public int Width => Image.Width;
/// <summary>
/// Gets the image height.
/// </summary>
public int Height => Image.Height;
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Gets or sets the <see cref="IResampler"/> that should
/// be used when scaling the image. Defaults to bicubic sampling.
/// </summary>
public IResampler? Resampler { get; set; }
internal SixLabors.ImageSharp.Image<Rgba32> Image { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CanvasImage"/> class.
/// </summary>
/// <param name="filename">The image filename.</param>
public CanvasImage(string filename)
{
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(filename);
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> 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);
}
}
}

View File

@ -0,0 +1,135 @@
using System;
using SixLabors.ImageSharp.Processing;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="CanvasImage"/>.
/// </summary>
public static class CanvasImageExtensions
{
/// <summary>
/// Sets the maximum width of the rendered image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage MaxWidth(this CanvasImage image, int? maxWidth)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.MaxWidth = maxWidth;
return image;
}
/// <summary>
/// Disables the maximum width of the rendered image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage NoMaxWidth(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.MaxWidth = null;
return image;
}
/// <summary>
/// Sets the pixel width.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="width">The pixel width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage PixelWidth(this CanvasImage image, int width)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.PixelWidth = width;
return image;
}
/// <summary>
/// Mutates the underlying image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="action">The action that mutates the underlying image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage Mutate(this CanvasImage image, Action<IImageProcessingContext> 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;
}
/// <summary>
/// Uses a bicubic sampler that implements the bicubic kernel algorithm W(x).
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage BicubicResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.Bicubic;
return image;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage BilinearResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.Triangle;
return image;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage NearestNeighborResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.NearestNeighbor;
return image;
}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
<Description>A library that extends Spectre.Console with ImageSharp super powers.</Description>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@ -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}

View File

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a renderable canvas.
/// </summary>
public sealed class Canvas : Renderable
{
private readonly Color?[,] _pixels;
/// <summary>
/// Gets the width of the canvas.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the canvas.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not
/// to scale the canvas when rendering.
/// </summary>
public bool Scale { get; set; } = true;
/// <summary>
/// Gets or sets the pixel width.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Initializes a new instance of the <see cref="Canvas"/> class.
/// </summary>
/// <param name="width">The canvas width.</param>
/// <param name="height">The canvas height.</param>
public Canvas(int width, int height)
{
Width = width;
Height = height;
_pixels = new Color?[Width, Height];
}
/// <summary>
/// Sets a pixel with the specified color in the canvas at the specified location.
/// </summary>
/// <param name="x">The X coordinate for the pixel.</param>
/// <param name="y">The Y coordinate for the pixel.</param>
/// <param name="color">The pixel color.</param>
public void SetPixel(int x, int y, Color color)
{
_pixels[x, y] = color;
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> 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;
}
}
}