UI/Logo.razor

A Razor UI panel that renders a colorful 'Breakout' wordmark made of clickable letter tiles. It defines two rows of tiles with default characters, colors and rotations, tracks per-tile overrides for color/rotation/hit count, and updates state on clicks to force re-render and trigger CSS animations.

Http Calls
@using System
@using Sandbox
@using Sandbox.UI
@namespace Breakout
@inherits Panel

<root>
    <div class="wordmark">
        <div class="tile-row">
            @for ( int i = 0; i < Row1.Length; i++ )
            {
                var t = Row1[i];
                var gi = i;
                <div class="tile" style="transform: rotate( @(RotOf( gi ))deg );">
                    <div class="tile-face @FaceClass( gi )" style="background-color: @ColorOf( gi );" onclick=@(() => TileClicked( gi ))>@t.Ch</div>
                </div>
            }
        </div>
        <div class="tile-row indent">
            @for ( int i = 0; i < Row2.Length; i++ )
            {
                var t = Row2[i];
                var gi = Row1.Length + i;
                <div class="tile" style="transform: rotate( @(RotOf( gi ))deg );">
                    <div class="tile-face @FaceClass( gi )" style="background-color: @ColorOf( gi );" onclick=@(() => TileClicked( gi ))>@t.Ch</div>
                </div>
            }
        </div>
    </div>
</root>

@code
{
    private static readonly (string Ch, string Color, int Rot)[] Row1 = new (string, string, int)[]
    {
        ("B", "#FF5D5D", -5),
        ("R", "#FF9E45", 4),
        ("E", "#FFC93C", -3),
        ("A", "#46D07E", 5),
        ("K", "#4A9BFF", -4)
    };

    private static readonly (string Ch, string Color, int Rot)[] Row2 = new (string, string, int)[]
    {
        ("O", "#9B7BFF", 4),
        ("U", "#FF7FB0", -5),
        ("T", "#FF5D5D", 3)
    };

    private static readonly string[] Palette = { "#FF5D5D", "#FF9E45", "#FFC93C", "#46D07E", "#4A9BFF", "#9B7BFF", "#FF7FB0" };

    // Per-tile overrides set when a tile is clicked. A null entry means "use the tile's default".
    // All three arrays are indexed the same way: Row1 letters first, then Row2 after them.
    private readonly string[] tileColors = new string[Row1.Length + Row2.Length];
    private readonly int?[] tileRots = new int?[Row1.Length + Row2.Length];
    private readonly int[] tileHits = new int[Row1.Length + Row2.Length];
    private int tileClicks;   // total clicks; only used to change BuildHash so the panel re-renders

    private string DefaultColor( int idx ) => idx < Row1.Length ? Row1[idx].Color : Row2[idx - Row1.Length].Color;
    private int DefaultRot( int idx ) => idx < Row1.Length ? Row1[idx].Rot : Row2[idx - Row1.Length].Rot;
    private string ColorOf( int idx ) => tileColors[idx] ?? DefaultColor( idx );
    private int RotOf( int idx ) => tileRots[idx] ?? DefaultRot( idx );

    // Alternate between two "hit" classes each click so the CSS wobble animation restarts every
    // time (re-adding the same class wouldn't replay it).
    private string FaceClass( int idx ) => tileHits[idx] == 0 ? "" : (tileHits[idx] % 2 == 1 ? "hit-a" : "hit-b");

    private void TileClicked( int idx )
    {
        // Pick a new random colour that isn't the current one (the guard just avoids repeats).
        var current = ColorOf( idx );
        var next = current;
        for ( int guard = 0; guard < 12 && next == current; guard++ )
            next = Palette[System.Random.Shared.Next( Palette.Length )];
        tileColors[idx] = next;

        // Pick a new tilt that's noticeably different from the current one.
        var curRot = RotOf( idx );
        var nextRot = curRot;
        for ( int guard = 0; guard < 12 && System.Math.Abs( nextRot - curRot ) < 5; guard++ )
            nextRot = System.Random.Shared.Next( -10, 11 );
        tileRots[idx] = nextRot;

        tileHits[idx]++;
        tileClicks++;
    }

    protected override int BuildHash() => tileClicks;
}