From 2a9fa223de5acd644307ae7f755c03071e1db85f Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Tue, 24 Nov 2020 21:24:21 +0100 Subject: [PATCH] Add canvas and image support Adds support for drawing "pixels" and displaying images in the terminal. --- README.md | 110 +++---------- examples/Canvas/Canvas.csproj | 22 +++ examples/Canvas/Mandelbrot.cs | 87 ++++++++++ examples/Canvas/Program.cs | 36 +++++ examples/Canvas/cake.png | Bin 0 -> 52832 bytes examples/Rules/Program.cs | 10 +- src/Spectre.Console.ImageSharp/CanvasImage.cs | 125 +++++++++++++++ .../CanvasImageExtensions.cs | 135 ++++++++++++++++ .../Spectre.Console.ImageSharp.csproj | 22 +++ src/Spectre.Console.sln | 29 ++++ src/Spectre.Console/Widgets/Canvas.cs | 151 ++++++++++++++++++ 11 files changed, 633 insertions(+), 94 deletions(-) create mode 100644 examples/Canvas/Canvas.csproj create mode 100644 examples/Canvas/Mandelbrot.cs create mode 100644 examples/Canvas/Program.cs create mode 100644 examples/Canvas/cake.png create mode 100644 src/Spectre.Console.ImageSharp/CanvasImage.cs create mode 100644 src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs create mode 100644 src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj create mode 100644 src/Spectre.Console/Widgets/Canvas.cs diff --git a/README.md b/README.md index 62790c8f..9b964cec 100644 --- a/README.md +++ b/README.md @@ -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! │ │ -└──────────┴──────────┴────────┘ -``` \ 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 00000000..a98bd150 --- /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 00000000..7f3a3da7 --- /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 00000000..337961da --- /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 0000000000000000000000000000000000000000..f11d285c9463d14b1c4250457fe2a358678392a7 GIT binary patch literal 52832 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%0AR{LQqrz*;V=tn8)n&-xSpEi6st^OJ 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 literal 0 HcmV?d00001 diff --git a/examples/Rules/Program.cs b/examples/Rules/Program.cs index e65ac975..8a03924c 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 00000000..fdf4beac --- /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 00000000..fa99fe6f --- /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 00000000..f5c18ad2 --- /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 c9a4ba39..a1af2ed7 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 00000000..acad4ceb --- /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; + } + } +}