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