Skip to content

Add canvas element #77

@TeseySTD

Description

@TeseySTD

Overview

I saw in Spectre.Console documentation a Canvas element, so I decided to add the component that wraps its logic.

Component overview

The component must accept the array of tuples with coordinates and Color of pixel, which it transforms in string representation and writes it as an data attribute.
I did so, but component has some memory issues with large sets of pixels.

I fixed that by implementing a registry, that stores pixels and provides it directly to html translator.

using System.Collections.Concurrent;
using Spectre.Console;

namespace RazorConsole.Core.Rendering;

public static class CanvasDataRegistry
{
    private static readonly ConcurrentDictionary<Guid, (int, int, Color)[]> Data = new();

    public static void Register(Guid id, (int, int, Color)[] data) => Data.AddOrUpdate(id, data, (_, _) => data);

    public static void Unregister(Guid id) => Data.TryRemove(id, out _);

    public static bool TryGetData(Guid id, out (int, int, Color)[]? data) => Data.TryGetValue(id, out data);
}

Here is the component's code:

@using RazorConsole.Core.Rendering
@using Spectre.Console
@namespace RazorConsole.Components
@implements IDisposable

<div class="spectre-canvas"
     data-canvas="true"
     data-width="@CanvasWidth"
     data-height="@CanvasHeight"
     data-maxwidth="@MaxWidthAttribute"
     data-pixelwidth="@PixelWidth"
     data-scale="@ScaleAttribute"
     data-canvas-data-id="@_dataId"
     data-update-token="@_updateToken"/>

@code {
    [Parameter]
    [EditorRequired]
    public (int x, int y, Color color)[] Pixels { get; set; } = [];

    [Parameter]
    [EditorRequired]
    public int CanvasWidth { get; set; }

    [Parameter]
    [EditorRequired]
    public int CanvasHeight { get; set; }

    [Parameter]
    public int? MaxWidth { get; set; }

    [Parameter]
    public int PixelWidth { get; set; } = 2;

    [Parameter]
    public bool Scale { get; set; }

    private string? MaxWidthAttribute => MaxWidth?.ToString();
    private string ScaleAttribute => Scale ? "true" : "false";

    private readonly Guid _dataId = Guid.NewGuid();
    private Guid _updateToken; // Token, that forces re-render when pixels array has changed
    private (int x, int y, Color color)[]? _lastPixelsReference;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ValidatePixelsLength();
        UpdateRegistry();
    }

    protected override void OnParametersSet()
    {
        base.OnParametersSet();
        ValidatePixelsLength();

        if (!ReferenceEquals(_lastPixelsReference, Pixels))
        {
            _updateToken = Guid.NewGuid();
            _lastPixelsReference = Pixels;
            UpdateRegistry();
        }
    }

    private void ValidatePixelsLength()
    {
        if (Pixels.Length > CanvasWidth * CanvasHeight)
        {
            throw new ArgumentException(
                $"Canvas pixels count ({Pixels.Length}) must be <= canvas area ({CanvasWidth * CanvasHeight}).");
        }
    }

    private void UpdateRegistry()
    {
        CanvasDataRegistry.Register(_dataId, Pixels);
    }

    public void Dispose()
    {
        CanvasDataRegistry.Unregister(_dataId);
    }
}

Animated Canvas

I also tried to implement an animated version of canvas, so I did, but I faced with some memory issues because of the frequent rendering of many canvases.

Maybe I will fix it, but now I'd like to provide the default canvas component.

P.S: I can make another issue for that, where I show my current AnimatedCanvas code and describe my problem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions