diff --git a/ShapeEngine/Geometry/RectDef/Rect.cs b/ShapeEngine/Geometry/RectDef/Rect.cs
index 957e7f97..b5160e20 100644
--- a/ShapeEngine/Geometry/RectDef/Rect.cs
+++ b/ShapeEngine/Geometry/RectDef/Rect.cs
@@ -38,6 +38,11 @@ namespace ShapeEngine.Geometry.RectDef;
///
public readonly float Height;
+ public override string ToString()
+ {
+ return $"Rect[X: {X}, Y: {Y}, Width: {Width}, Height: {Height}]";
+ }
+
#endregion
#region Getter Setter
diff --git a/ShapeEngine/Text/BinaryDrawer.cs b/ShapeEngine/Text/BinaryDrawer.cs
deleted file mode 100644
index c3edcc5a..00000000
--- a/ShapeEngine/Text/BinaryDrawer.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using ShapeEngine.Color;
-using ShapeEngine.Core.Structs;
-using ShapeEngine.Geometry.RectDef;
-
-namespace ShapeEngine.Text;
-
-///
-/// A tester class for the binary drawer.
-///
-public static class BinaryDrawerTester
-{
- ///
- /// A standard 3x5 binary drawer.
- ///
- public static readonly BinaryDrawer BinaryDrawer3x5Standard =
- new BinaryDrawer
- (
- new Dictionary()
- {
- {'0', new [] { 1,1,1,1,0,1,1,0,1,1,0,1,1,1,1 }},
- {'1', new [] { 0,1,0,0,1,0,0,1,0,0,1,0,0,1,0 }},
- {'2', new [] { 1,1,1,0,0,1,1,1,1,1,0,0,1,1,1 }},
- {'3', new [] { 1,1,1,0,0,1,0,1,1,0,0,1,1,1,1 }},
- {'4', new [] { 1,0,0,1,0,0,1,1,1,0,1,0,0,1,0 }},
- {'5', new [] { 1,1,1,1,0,0,1,1,1,0,0,1,1,1,1 }},
- {'6', new [] { 1,0,0,1,0,0,1,1,1,1,0,1,1,1,1 }},
- {'7', new [] { 1,1,1,0,0,1,0,1,1,0,0,1,0,0,1 }},
- {'8', new [] { 1,1,1,1,0,1,1,1,1,1,0,1,1,1,1 }},
- {'9', new [] { 1,1,1,1,0,1,1,1,1,0,0,1,0,0,1 }},
- },
- 3, 5,
- new((rect, code, index, maxIndex) =>
- {
- if (code > 0)
- {
- if (index == 1 || index == 7 || index == 13)
- {
- rect.ApplyMargins(0f, 0f, 0.1f, 0.1f).Draw(new ColorRgba(255, 0, 0, 255));
- }
- else rect.ApplyMargins(0.15f, 0.15f, 0f, 0f).Draw(new ColorRgba(255, 0, 0, 255));
- // rect.DrawLines(rect.Size.Min() * 0.1f, ColorRgba.White);
-
- // rect.Draw(new ColorRgba(255, 0, 0, 255));
- }
- }),
- new((rect, c) => { })
- );
-}
-
-///
-/// Draws text based on a binary grid for each character.
-///
-public class BinaryDrawer
-{
- private readonly Dictionary binary;
- private readonly int gridWidth;
- private readonly int gridHeight;
- private readonly Action cellDrawer;
- private readonly Action backgroundDrawer;
- ///
- /// The total size of the grid (width * height).
- ///
- public int GridSize => gridWidth * gridHeight;
-
- ///
- /// Creates a new binary drawer.
- ///
- /// The dictionary that maps characters to their binary representation.
- /// The width of the character grid.
- /// The height of the character grid.
- /// The action to draw each cell of the character.
- /// The action to draw the background for each character.
- public BinaryDrawer(Dictionary binary, int gridWidth, int gridHeight, Action cellDrawer, Action backgroundDrawer)
- {
- this.binary = binary;
- this.gridWidth = gridWidth;
- this.gridHeight = gridHeight;
- this.cellDrawer = cellDrawer;
- this.backgroundDrawer = backgroundDrawer;
- }
-
- ///
- /// Draws the given text inside the given rectangle.
- ///
- /// The text to draw.
- /// The rectangle to draw the text in.
- /// The spacing between characters.
- public void Draw(string text, Rect rect, float spacing = 0.05f)
- {
- var chars = text.ToCharArray();
- // var split = rect.SplitH(chars.Length);
- var split = rect.GetAlignedRectsGrid(new Grid(chars.Length, 1), new Size(spacing, 0f));
- if (split == null) return;
-
-
- for (int i = 0; i < chars.Length; i++)
- {
- var c = chars[i];
- if(!binary.TryGetValue(c, out int[]? binaryCode)) continue;
- backgroundDrawer(split[i], c);
- var grid = split[i].Split(gridWidth, gridHeight);
- for (int j = 0; j < grid.Count; j++)
- {
- cellDrawer(grid[j], binaryCode[j], j, GridSize);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/ShapeEngine/Text/BitmapFont.cs b/ShapeEngine/Text/BitmapFont.cs
new file mode 100644
index 00000000..3a9efb0b
--- /dev/null
+++ b/ShapeEngine/Text/BitmapFont.cs
@@ -0,0 +1,609 @@
+using System.Numerics;
+using ShapeEngine.Color;
+using ShapeEngine.Core.Structs;
+using ShapeEngine.Geometry.RectDef;
+
+namespace ShapeEngine.Text;
+
+///
+/// Simple bitmap font drawer using string arrays for each character.
+///
+public partial class BitmapFont
+{
+ #region Properties
+
+ private readonly Dictionary fontMap;
+ private readonly int gridWidth;
+ private readonly int gridHeight;
+
+ ///
+ /// Gets the number of characters defined in the font.
+ ///
+ public int Count => fontMap.Count;
+
+ ///
+ /// Returns all characters available in the font.
+ ///
+ public List SupportedChars => fontMap.Keys.ToList();
+
+ ///
+ /// Returns all characters available in the font.
+ ///
+ public HashSet SupportedCharsSet => fontMap.Keys.ToHashSet();
+
+ ///
+ /// Gets the grid representation for the specified character.
+ /// Returns null if the character is not defined in the font.
+ ///
+ /// The character to retrieve the grid for.
+ /// String array representing the character grid, or null if not found.
+ public string[]? GetGrid(char c) => fontMap.GetValueOrDefault(c);
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the BitmapFontRenderer.
+ ///
+ ///
+ /// Dictionary mapping characters to their grid representation as string arrays.
+ /// Can not be empty and has to contain all characters that will be drawn.
+ ///
+ ///
+ /// All characters need to have the same amount of rows (string array entries) and columns (string length).
+ ///
+ public BitmapFont(Dictionary fontMap)
+ {
+ if (fontMap == null || fontMap.Count == 0)
+ throw new ArgumentException("Font map cannot be null or empty.");
+
+ int expectedRows = fontMap.Values.First().Length;
+ int expectedCols = fontMap.Values.First()[0].Length;
+
+ foreach (var entry in fontMap)
+ {
+ var grid = entry.Value;
+ if (grid.Length != expectedRows)
+ throw new ArgumentException($"Character '{entry.Key}' does not have {expectedRows} rows.");
+
+ foreach (var row in grid)
+ {
+ if (row.Length != expectedCols)
+ throw new ArgumentException($"Character '{entry.Key}' has a row with incorrect length (expected {expectedCols}).");
+ }
+ }
+
+ this.fontMap = fontMap;
+ gridHeight = expectedRows;
+ gridWidth = expectedCols;
+ }
+ private BitmapFont(Dictionary fontMap, int gridWidth, int gridHeight)
+ {
+ this.fontMap = fontMap;
+ this.gridWidth = gridWidth;
+ this.gridHeight = gridHeight;
+ }
+
+ #endregion
+
+ #region Copy / Filter
+
+ ///
+ /// Creates a deep copy of the current .
+ ///
+ /// A new instance with copied font map and grid dimensions.
+ public BitmapFont Copy()
+ {
+ var newFontMap = new Dictionary(fontMap.Count);
+ foreach (var kvp in fontMap)
+ {
+ var gridCopy = new string[kvp.Value.Length];
+ Array.Copy(kvp.Value, gridCopy, kvp.Value.Length);
+ newFontMap[kvp.Key] = gridCopy;
+ }
+ return new BitmapFont(newFontMap, gridWidth, gridHeight);
+ }
+ ///
+ /// Returns a new containing only the specified supported characters.
+ ///
+ /// A list of characters to include in the filtered font.
+ /// A new instance with only the supported characters.
+ public BitmapFont Filter(List supportedChars)
+ {
+ var newFontMap = new Dictionary();
+ foreach (var kvp in fontMap)
+ {
+ if (!supportedChars.Contains(kvp.Key)) continue;
+ var gridCopy = new string[kvp.Value.Length];
+ Array.Copy(kvp.Value, gridCopy, kvp.Value.Length);
+ newFontMap[kvp.Key] = gridCopy;
+ }
+ return new BitmapFont(newFontMap, gridWidth, gridHeight);
+ }
+ ///
+ /// Returns a new containing only the specified supported characters.
+ ///
+ /// A hash set of characters to include in the filtered font.
+ /// A new instance with only the supported characters.
+ public BitmapFont Filter(HashSet supportedChars)
+ {
+ var newFontMap = new Dictionary();
+ foreach (var kvp in fontMap)
+ {
+ if (!supportedChars.Contains(kvp.Key)) continue;
+ var gridCopy = new string[kvp.Value.Length];
+ Array.Copy(kvp.Value, gridCopy, kvp.Value.Length);
+ newFontMap[kvp.Key] = gridCopy;
+ }
+ return new BitmapFont(newFontMap, gridWidth, gridHeight);
+ }
+
+ #endregion
+
+ #region Draw Char
+
+ ///
+ /// Draws a single character into the specified rectangle using the provided color.
+ ///
+ /// The character to draw.
+ /// The rectangle area in which to draw the character.
+ /// The color to use for each filled cell.
+ ///
+ /// Only cells marked as '1' in the character grid are drawn. If the rectangle is too small, nothing is drawn.
+ ///
+ public void Draw(char c, Rect rect, ColorRgba cellColor)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (!fontMap.TryGetValue(c, out string[]? grid)) return;
+ var cellRects = rect.Split(gridWidth, gridHeight);
+ for (var row = 0; row < gridHeight; row++)
+ {
+ for (var col = 0; col < gridWidth; col++)
+ {
+ if (grid[row][col] != '1') continue;
+ int cellIndex = row * gridWidth + col;
+ cellRects[cellIndex].Draw(cellColor);
+ }
+ }
+ }
+ ///
+ /// Draws a single character into the specified rectangle using a custom cell drawing action.
+ ///
+ /// The character to draw.
+ /// The rectangle area in which to draw the character.
+ /// Action to invoke for each cell, providing the cell rectangle, a bool indicating if the cell is filled, the character, row, and column.
+ ///
+ /// If the rectangle is too small, nothing is drawn.
+ ///
+ public void Draw(char c, Rect rect, Action drawCell)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (!fontMap.TryGetValue(c, out string[]? grid)) return;
+ var cellRects = rect.Split(gridWidth, gridHeight);
+ for (var row = 0; row < gridHeight; row++)
+ {
+ for (var col = 0; col < gridWidth; col++)
+ {
+ int cellIndex = row * gridWidth + col;
+ drawCell(cellRects[cellIndex], grid[row][col] == '1', c, row, col);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Draw
+ ///
+ /// Draws a string of text into the specified rectangle using the provided color.
+ ///
+ /// The text to draw.
+ /// The rectangle area in which to draw the text.
+ /// The color to use for each filled cell.
+ /// Spacing between characters, relative to character width.
+ ///
+ /// Each character is drawn in sequence, spaced according to the spacing parameter. Only cells marked as '1' are drawn.
+ ///
+ public void Draw(string text, Rect rect, ColorRgba cellColor, float spacing = 0.05f)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (string.IsNullOrEmpty(text)) return;
+ var chars = text.ToCharArray();
+ int charCount = chars.Length;
+ if (charCount == 0) return;
+
+ float charWidth = rect.Size.Width / (charCount + (charCount - 1) * spacing);
+ float charHeight = rect.Size.Height;
+ float cellWidth = charWidth / gridWidth;
+ float cellHeight = charHeight / gridHeight;
+
+ float x = rect.TopLeft.X;
+ float y = rect.TopLeft.Y;
+
+ for (int i = 0; i < charCount; i++)
+ {
+ var c = chars[i];
+ if (!fontMap.TryGetValue(c, out var grid)) continue;
+
+ for (int row = 0; row < gridHeight; row++)
+ {
+ for (int col = 0; col < gridWidth; col++)
+ {
+ if (grid[row][col] == '1')
+ {
+ float cellX = x + col * cellWidth;
+ float cellY = y + row * cellHeight;
+ var cellRect = new Rect(
+ new Vector2(cellX, cellY),
+ new Size(cellWidth, cellHeight),
+ AnchorPoint.TopLeft
+ );
+ cellRect.Draw(cellColor);
+ }
+ }
+ }
+ x += charWidth + spacing * charWidth;
+ }
+ }
+
+ ///
+ /// Draws a string of text into the specified rectangle using a custom cell drawing action.
+ ///
+ /// The text to draw.
+ /// The rectangle area in which to draw the text.
+ /// Action to invoke for each cell, providing the cell rectangle, a bool indicating if the cell is filled, the character, row, and column.
+ /// Spacing between characters, relative to character width.
+ ///
+ /// Each character is drawn in sequence, spaced according to the spacing parameter. Only cells marked as '1' are drawn.
+ ///
+ public void Draw(string text, Rect rect, Action drawCell, float spacing = 0.05f)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (string.IsNullOrEmpty(text)) return;
+ var chars = text.ToCharArray();
+ int charCount = chars.Length;
+ if (charCount == 0) return;
+
+ float charWidth = rect.Size.Width / (charCount + (charCount - 1) * spacing);
+ float charHeight = rect.Size.Height;
+ float cellWidth = charWidth / gridWidth;
+ float cellHeight = charHeight / gridHeight;
+
+ float x = rect.TopLeft.X;
+ float y = rect.TopLeft.Y;
+
+ for (int i = 0; i < charCount; i++)
+ {
+ var c = chars[i];
+ if (!fontMap.TryGetValue(c, out var grid)) continue;
+
+ for (int row = 0; row < gridHeight; row++)
+ {
+ for (int col = 0; col < gridWidth; col++)
+ {
+ float cellX = x + col * cellWidth;
+ float cellY = y + row * cellHeight;
+ var cellRect = new Rect(
+ new Vector2(cellX, cellY),
+ new Size(cellWidth, cellHeight),
+ AnchorPoint.TopLeft
+ );
+ bool filled = grid[row][col] == '1';
+ drawCell(cellRect, filled, c, row, col);
+
+ }
+ }
+ x += charWidth + spacing * charWidth;
+ }
+ }
+
+ ///
+ /// Draws a string of text into the specified rectangle with uniform scaling and alignment, using the provided color.
+ ///
+ /// The text to draw.
+ /// The rectangle area in which to draw the text.
+ /// The color to use for each filled cell.
+ /// Anchor point for text alignment within the rectangle.
+ /// Spacing between characters, relative to character width.
+ ///
+ /// Uniform scaling ensures all characters are the same size and aligned according to the specified anchor point.
+ ///
+ public void DrawUniform(string text, Rect rect, ColorRgba cellColor, AnchorPoint alignment, float spacing = 0.05f)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (string.IsNullOrEmpty(text)) return;
+ var chars = text.ToCharArray();
+ int charCount = chars.Length;
+ if (charCount == 0) return;
+
+ // Uniform scaling using gridWidth/gridHeight ratio
+ float totalGridWidth = charCount * gridWidth + (charCount - 1) * spacing * gridWidth;
+ float scaleX = rect.Size.Width / totalGridWidth;
+ float scaleY = rect.Size.Height / gridHeight;
+ float scale = Math.Min(scaleX, scaleY);
+
+ float charWidth = scale * gridWidth;
+ float charHeight = scale * gridHeight;
+ float cellWidth = charWidth / gridWidth;
+ float cellHeight = charHeight / gridHeight;
+
+ float textWidth = charCount * charWidth + (charCount - 1) * spacing * charWidth;
+ float textHeight = charHeight;
+
+ // Use AnchorPoint alignment for offset
+ float offsetX = (rect.Size.Width - textWidth) * alignment.X;
+ float offsetY = (rect.Size.Height - textHeight) * alignment.Y;
+ float x = rect.TopLeft.X + offsetX;
+ float y = rect.TopLeft.Y + offsetY;
+
+ for (int i = 0; i < charCount; i++)
+ {
+ var c = chars[i];
+ if (!fontMap.TryGetValue(c, out var grid)) continue;
+
+ for (int row = 0; row < gridHeight; row++)
+ {
+ for (int col = 0; col < gridWidth; col++)
+ {
+ if (grid[row][col] == '1')
+ {
+ float cellX = x + col * cellWidth;
+ float cellY = y + row * cellHeight;
+ var cellRect = new Rect(
+ new Vector2(cellX, cellY),
+ new Size(cellWidth, cellHeight),
+ AnchorPoint.TopLeft
+ );
+ cellRect.Draw(cellColor);
+ }
+ }
+ }
+ x += charWidth + spacing * charWidth;
+ }
+ }
+
+ ///
+ /// Draws a string of text into the specified rectangle with uniform scaling and alignment, using a custom cell drawing action.
+ ///
+ /// The text to draw.
+ /// The rectangle area in which to draw the text.
+ /// Action to invoke for each cell, providing the cell rectangle, a bool indicating if the cell is filled, the character, row, and column.
+ /// Anchor point for text alignment within the rectangle.
+ /// Spacing between characters, relative to character width.
+ ///
+ /// Uniform scaling ensures all characters are the same size and aligned according to the specified anchor point.
+ ///
+ public void DrawUniform(string text, Rect rect, Action drawCell, AnchorPoint alignment, float spacing = 0.05f)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (string.IsNullOrEmpty(text)) return;
+ var chars = text.ToCharArray();
+ int charCount = chars.Length;
+ if (charCount == 0) return;
+
+ // Uniform scaling using gridWidth/gridHeight ratio
+ float totalGridWidth = charCount * gridWidth + (charCount - 1) * spacing * gridWidth;
+ float scaleX = rect.Size.Width / totalGridWidth;
+ float scaleY = rect.Size.Height / gridHeight;
+ float scale = Math.Min(scaleX, scaleY);
+
+ float charWidth = scale * gridWidth;
+ float charHeight = scale * gridHeight;
+ float cellWidth = charWidth / gridWidth;
+ float cellHeight = charHeight / gridHeight;
+
+ float textWidth = charCount * charWidth + (charCount - 1) * spacing * charWidth;
+ float textHeight = charHeight;
+
+ // Use AnchorPoint alignment for offset
+ float offsetX = (rect.Size.Width - textWidth) * alignment.X;
+ float offsetY = (rect.Size.Height - textHeight) * alignment.Y;
+ float x = rect.TopLeft.X + offsetX;
+ float y = rect.TopLeft.Y + offsetY;
+
+ for (int i = 0; i < charCount; i++)
+ {
+ var c = chars[i];
+ if (!fontMap.TryGetValue(c, out var grid)) continue;
+
+ for (int row = 0; row < gridHeight; row++)
+ {
+ for (int col = 0; col < gridWidth; col++)
+ {
+ float cellX = x + col * cellWidth;
+ float cellY = y + row * cellHeight;
+ var cellRect = new Rect(
+ new Vector2(cellX, cellY),
+ new Size(cellWidth, cellHeight),
+ AnchorPoint.TopLeft
+ );
+ bool filled = grid[row][col] == '1';
+ drawCell(cellRect, filled, c, row, col);
+ }
+ }
+ x += charWidth + spacing * charWidth;
+ }
+ }
+ #endregion
+
+ #region Draw With Wrap
+
+ ///
+ /// Draws a string of text into the specified rectangle, wrapping text to multiple lines as needed, using the provided color.
+ ///
+ /// The text to draw.
+ /// The rectangle area in which to draw the text.
+ /// The color to use for each filled cell.
+ /// Spacing between characters, relative to character width.
+ /// Spacing between lines, relative to character height.
+ ///
+ /// Automatically determines the optimal number of characters per line to maximize area usage. Only cells marked as '1' are drawn.
+ ///
+ public void DrawWithWrap(string text, Rect rect, ColorRgba cellColor, float charSpacing = 0.05f, float lineSpacing = 0.05f)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (string.IsNullOrEmpty(text)) return;
+ var chars = text.ToCharArray();
+ int charCount = chars.Length;
+ if (charCount == 0) return;
+
+ // Try all possible charsPerLine to maximize area usage
+ int bestCharsPerLine = 1;
+ float bestCharHeight = 0f;
+ float bestCharWidth = 0f;
+ int bestLines = charCount;
+ for (int charsPerLine = 1; charsPerLine <= charCount; charsPerLine++)
+ {
+ int lines = (int)Math.Ceiling((float)charCount / charsPerLine);
+ float totalSpacingX = (charsPerLine - 1) * charSpacing;
+ float totalSpacingY = (lines - 1) * lineSpacing;
+ float charWidth = rect.Size.Width / (charsPerLine + totalSpacingX);
+ float charHeight = rect.Size.Height / (lines + totalSpacingY);
+ // Maintain aspect ratio
+ if (charHeight > 0 && charWidth > 0 && charHeight < charWidth * gridHeight / gridWidth)
+ {
+ charWidth = charHeight * gridWidth / gridHeight;
+ }
+ else
+ {
+ charHeight = charWidth * gridHeight / gridWidth;
+ }
+ if (charHeight > bestCharHeight)
+ {
+ bestCharHeight = charHeight;
+ bestCharWidth = charWidth;
+ bestCharsPerLine = charsPerLine;
+ bestLines = lines;
+ }
+ }
+
+ float cellWidth = bestCharWidth / gridWidth;
+ float cellHeight = bestCharHeight / gridHeight;
+
+ float totalTextHeight = bestLines * bestCharHeight + (bestLines - 1) * lineSpacing * bestCharHeight;
+ float offsetY = rect.TopLeft.Y + (rect.Size.Height - totalTextHeight) * 0.5f;
+
+ int charIndex = 0;
+ for (int line = 0; line < bestLines; line++)
+ {
+ int charsThisLine = Math.Min(bestCharsPerLine, charCount - charIndex);
+ float totalLineWidth = charsThisLine * bestCharWidth + (charsThisLine - 1) * charSpacing * bestCharWidth;
+ float offsetX = rect.TopLeft.X + (rect.Size.Width - totalLineWidth) * 0.5f;
+ float y = offsetY + line * (bestCharHeight + lineSpacing * bestCharHeight);
+ float x = offsetX;
+ for (int i = 0; i < charsThisLine; i++)
+ {
+ var c = chars[charIndex++];
+ if (!fontMap.TryGetValue(c, out var grid)) continue;
+ for (int row = 0; row < gridHeight; row++)
+ {
+ for (int col = 0; col < gridWidth; col++)
+ {
+ if (grid[row][col] == '1')
+ {
+ float cellX = x + col * cellWidth;
+ float cellY = y + row * cellHeight;
+ var cellRect = new Rect(
+ new Vector2(cellX, cellY),
+ new Size(cellWidth, cellHeight),
+ AnchorPoint.TopLeft
+ );
+ cellRect.Draw(cellColor);
+ }
+ }
+ }
+ x += bestCharWidth + charSpacing * bestCharWidth;
+ }
+ }
+ }
+
+ ///
+ /// Draws a string of text into the specified rectangle, wrapping text to multiple lines as needed, using a custom cell drawing action.
+ ///
+ /// The text to draw.
+ /// The rectangle area in which to draw the text.
+ /// Action to invoke for each cell, providing the cell rectangle, a bool indicating if the cell is filled, the character, row, and column.
+ /// Spacing between characters, relative to character width.
+ /// Spacing between lines, relative to character height.
+ ///
+ /// Automatically determines the optimal number of characters per line to maximize area usage. Only cells marked as '1' are drawn.
+ ///
+ public void DrawWithWrap(string text, Rect rect, Action drawCell, float charSpacing = 0.05f, float lineSpacing = 0.05f)
+ {
+ if(rect.Width <= 1 || rect.Height <= 1) return;
+ if (string.IsNullOrEmpty(text)) return;
+ var chars = text.ToCharArray();
+ int charCount = chars.Length;
+ if (charCount == 0) return;
+
+ // Try all possible charsPerLine to maximize area usage
+ int bestCharsPerLine = 1;
+ float bestCharHeight = 0f;
+ float bestCharWidth = 0f;
+ int bestLines = charCount;
+ for (int charsPerLine = 1; charsPerLine <= charCount; charsPerLine++)
+ {
+ int lines = (int)Math.Ceiling((float)charCount / charsPerLine);
+ float totalSpacingX = (charsPerLine - 1) * charSpacing;
+ float totalSpacingY = (lines - 1) * lineSpacing;
+ float charWidth = rect.Size.Width / (charsPerLine + totalSpacingX);
+ float charHeight = rect.Size.Height / (lines + totalSpacingY);
+ // Maintain aspect ratio
+ if (charHeight > 0 && charWidth > 0 && charHeight < charWidth * gridHeight / gridWidth)
+ {
+ charWidth = charHeight * gridWidth / gridHeight;
+ }
+ else
+ {
+ charHeight = charWidth * gridHeight / gridWidth;
+ }
+ if (charHeight > bestCharHeight)
+ {
+ bestCharHeight = charHeight;
+ bestCharWidth = charWidth;
+ bestCharsPerLine = charsPerLine;
+ bestLines = lines;
+ }
+ }
+
+ float cellWidth = bestCharWidth / gridWidth;
+ float cellHeight = bestCharHeight / gridHeight;
+
+ float totalTextHeight = bestLines * bestCharHeight + (bestLines - 1) * lineSpacing * bestCharHeight;
+ float offsetY = rect.TopLeft.Y + (rect.Size.Height - totalTextHeight) * 0.5f;
+
+ int charIndex = 0;
+ for (int line = 0; line < bestLines; line++)
+ {
+ int charsThisLine = Math.Min(bestCharsPerLine, charCount - charIndex);
+ float totalLineWidth = charsThisLine * bestCharWidth + (charsThisLine - 1) * charSpacing * bestCharWidth;
+ float offsetX = rect.TopLeft.X + (rect.Size.Width - totalLineWidth) * 0.5f;
+ float y = offsetY + line * (bestCharHeight + lineSpacing * bestCharHeight);
+ float x = offsetX;
+ for (int i = 0; i < charsThisLine; i++)
+ {
+ var c = chars[charIndex++];
+ if (!fontMap.TryGetValue(c, out var grid)) continue;
+ for (int row = 0; row < gridHeight; row++)
+ {
+ for (int col = 0; col < gridWidth; col++)
+ {
+ float cellX = x + col * cellWidth;
+ float cellY = y + row * cellHeight;
+ var cellRect = new Rect(
+ new Vector2(cellX, cellY),
+ new Size(cellWidth, cellHeight),
+ AnchorPoint.TopLeft
+ );
+ bool filled = grid[row][col] == '1';
+ drawCell(cellRect, filled, c, row, col);
+ }
+ }
+ x += bestCharWidth + charSpacing * bestCharWidth;
+ }
+ }
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/ShapeEngine/Text/BitmapFontAtlas.cs b/ShapeEngine/Text/BitmapFontAtlas.cs
new file mode 100644
index 00000000..3fff6970
--- /dev/null
+++ b/ShapeEngine/Text/BitmapFontAtlas.cs
@@ -0,0 +1,500 @@
+using System.Numerics;
+using Raylib_cs;
+using ShapeEngine.Color;
+using ShapeEngine.Core.Structs;
+using ShapeEngine.Geometry.RectDef;
+
+namespace ShapeEngine.Text;
+
+public class BitmapFontAtlas
+{
+ #region Properties
+
+ private const int Padding = 2; // Padding between glyphs in the atlas
+
+ private readonly Dictionary glyphUvRects = new(); // UV rects for each character
+ private readonly int atlasWidth;
+ private readonly int atlasHeight;
+ private RenderTexture2D atlasTexture;
+ private readonly BitmapFont font;
+ private readonly int glyphWidth;
+ private readonly int glyphHeight;
+ private readonly List supportedChars;
+ private readonly int gridRows;
+ private readonly int gridCols;
+
+ ///
+ /// Indicates whether the font atlas has been generated and is ready for use.
+ ///
+ public bool IsGenerated { get; private set; }
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the BitmapFontAtlas class.
+ ///
+ /// The bitmap font containing supported characters.
+ /// The width of each glyph in the atlas.
+ /// The height of each glyph in the atlas.
+ ///
+ /// Filtering of supported characters is handled in the BitmapFont class.
+ ///
+ public BitmapFontAtlas(BitmapFont font, int glyphWidth, int glyphHeight)
+ {
+ if (font.Count == 0) throw new ArgumentException("Font or font map is null or empty.");
+ if (glyphWidth <= 0 || glyphHeight <= 0) throw new ArgumentException("Glyph size must be positive.");
+
+ supportedChars = font.SupportedChars;
+
+ if (supportedChars.Count == 0) throw new ArgumentException("No characters available for atlas generation.");
+
+ this.font = font;
+ int totalGlyphs = supportedChars.Count;
+
+ // Calculate optimal grid size
+ (gridRows, gridCols) = CalculateGrid(totalGlyphs);
+
+ atlasWidth = Padding + gridCols * (glyphWidth + Padding);
+ atlasHeight = Padding + gridRows * (glyphHeight + Padding);
+
+ this.glyphWidth = glyphWidth;
+ this.glyphHeight = glyphHeight;
+
+ if (atlasWidth <= 0 || atlasHeight <= 0)
+ throw new ArgumentException("Calculated atlas size is invalid. Check atlas size and character count.");
+ }
+
+ #endregion
+
+ #region Generate Atlas
+
+ ///
+ /// Generates the font atlas texture by drawing each glyph with the specified color and background.
+ /// Throws an exception if the atlas is already generated.
+ ///
+ /// The color to use for glyphs.
+ /// The background color of the atlas.
+ public void GenerateAtlas(ColorRgba glyphColor, ColorRgba backgroundColor)
+ {
+ if (IsGenerated) throw new InvalidOperationException("Atlas already generated.");
+ atlasTexture = Raylib.LoadRenderTexture(atlasWidth, atlasHeight);
+ Console.WriteLine($"Atlas generation started with atlas size: {atlasWidth}x{atlasHeight}, grid: {gridRows}x{gridCols}");
+ Raylib.BeginTextureMode(atlasTexture);
+ Raylib.DrawRectangle(0, 0, atlasWidth, atlasHeight, backgroundColor.ToRayColor());
+ int i = 0;
+
+ // Generate atlas in normal top-to-bottom order
+ for (int row = 0; row < gridRows; row++)
+ {
+ for (int col = 0; col < gridCols; col++)
+ {
+ if (i >= supportedChars.Count) break;
+ var c = supportedChars[i];
+ var glyphRect = new Rect(
+ new Vector2(Padding + col * (glyphWidth + Padding), Padding + row * (glyphHeight + Padding)),
+ new Size(glyphWidth, glyphHeight),
+ AnchorPoint.TopLeft
+ );
+ glyphUvRects[c] = glyphRect;
+ font.Draw(c, glyphRect, glyphColor);
+ Console.WriteLine($"Glyph '{c}' at row {row}, col {col} with {glyphRect}");
+ i++;
+ }
+ if (i >= supportedChars.Count) break;
+ }
+ Raylib.EndTextureMode();
+ IsGenerated = true;
+ Raylib.SetTextureFilter(atlasTexture.Texture, TextureFilter.Point);
+ Console.WriteLine("Atlas generation completed.");
+ }
+ ///
+ /// Generates the font atlas texture using a custom cell drawing action.
+ /// Throws an exception if the atlas is already generated.
+ ///
+ /// The background color of the atlas.
+ ///
+ /// An action to draw each cell, with parameters:
+ ///
+ /// - Rect: The rectangle for the glyph cell.
+ /// - bool: Indicates if the cell is active.
+ /// - char: The character for the glyph.
+ /// - int: The row index.
+ /// - int: The column index.
+ ///
+ ///
+ public void GenerateAtlas(ColorRgba backgroundColor, Action drawCell)
+ {
+ if (IsGenerated) throw new InvalidOperationException("Atlas already generated.");
+ atlasTexture = Raylib.LoadRenderTexture(atlasWidth, atlasHeight);
+ Console.WriteLine($"Atlas generation started with atlas size: {atlasWidth}x{atlasHeight}, grid: {gridRows}x{gridCols}");
+ Raylib.BeginTextureMode(atlasTexture);
+ Raylib.DrawRectangle(0, 0, atlasWidth, atlasHeight, backgroundColor.ToRayColor());
+ int i = 0;
+
+ // Generate atlas in normal top-to-bottom order
+ for (int row = 0; row < gridRows; row++)
+ {
+ for (int col = 0; col < gridCols; col++)
+ {
+ if (i >= supportedChars.Count) break;
+ var c = supportedChars[i];
+ var glyphRect = new Rect(
+ new Vector2(Padding + col * (glyphWidth + Padding), Padding + row * (glyphHeight + Padding)),
+ new Size(glyphWidth, glyphHeight),
+ AnchorPoint.TopLeft
+ );
+ glyphUvRects[c] = glyphRect;
+ font.Draw(c, glyphRect, drawCell);
+ Console.WriteLine($"Glyph '{c}' at row {row}, col {col} with custom {glyphRect}");
+ i++;
+ }
+ if (i >= supportedChars.Count) break;
+ }
+ Raylib.EndTextureMode();
+ IsGenerated = true;
+ Raylib.SetTextureFilter(atlasTexture.Texture, TextureFilter.Point);
+ Console.WriteLine("Atlas generation completed.");
+ }
+
+
+ ///
+ /// Draws the entire atlas texture at the specified position and scale, applying the given tint color.
+ /// The atlas is vertically flipped to match the expected orientation.
+ ///
+ /// The position on the screen where the atlas will be drawn.
+ /// The scale factor to apply to the atlas texture.
+ /// The color tint to apply to the atlas texture.
+ public void DrawAtlas(Vector2 position, Vector2 scale, ColorRgba tint)
+ {
+ if (!IsGenerated) throw new InvalidOperationException("Atlas not generated.");
+
+ // Flip the atlas texture vertically when drawing
+ var srcRect = new Rectangle(0, 0, atlasWidth, -atlasHeight);
+ var destRect = new Rectangle(position.X, position.Y, atlasWidth * scale.X, atlasHeight * scale.Y);
+ var origin = new Vector2(0, 0);
+ const float rotation = 0f;
+ Raylib.DrawTexturePro(atlasTexture.Texture, srcRect, destRect, origin, rotation, tint.ToRayColor());
+ }
+
+ ///
+ /// Draws the entire atlas texture so it fits into the given rectangle, wrapping glyphs to new lines as needed.
+ /// The glyphs are drawn from top-left to bottom-right, with each glyph placed in order of supportedChars.
+ ///
+ /// The rectangle area to fit the atlas into.
+ /// The color tint to apply to the atlas texture.
+ /// Optional padding around each glyph to prevent overlap.
+ public void DrawAtlas(Rect rect, ColorRgba tint, int padding = 8)
+ {
+ if (!IsGenerated) throw new InvalidOperationException("Atlas not generated.");
+
+ // Calculate dynamic grid layout for the given rectangle (original formula)
+ int glyphsPerRow = Math.Min(supportedChars.Count, Math.Max(1, (int)Math.Sqrt(rect.Size.Width * supportedChars.Count / rect.Size.Height)));
+ int rows = (int)Math.Ceiling((double)supportedChars.Count / glyphsPerRow);
+
+ float paddedGlyphWidth = glyphWidth + 2 * padding;
+ float paddedGlyphHeight = glyphHeight + 2 * padding;
+ float scaleX = rect.Size.Width / (glyphsPerRow * paddedGlyphWidth);
+ float scaleY = rect.Size.Height / (rows * paddedGlyphHeight);
+ float scale = Math.Min(scaleX, scaleY);
+
+ // Calculate centering offsets to center the grid within the rectangle
+ float actualGridWidth = glyphsPerRow * paddedGlyphWidth * scale;
+ float actualGridHeight = rows * paddedGlyphHeight * scale;
+ float offsetX = rect.TopLeft.X + (rect.Size.Width - actualGridWidth) / 2f;
+ float offsetY = rect.TopLeft.Y + (rect.Size.Height - actualGridHeight) / 2f;
+
+ // Draw characters in order from supportedChars, using their UV rectangles from atlas
+ for (int charIndex = 0; charIndex < supportedChars.Count; charIndex++)
+ {
+ int row = charIndex / glyphsPerRow;
+ int col = charIndex % glyphsPerRow;
+
+ char ch = supportedChars[charIndex];
+ if (!glyphUvRects.TryGetValue(ch, out var uvRect)) continue;
+
+ var srcRect = GenerateSourceRectangle(uvRect);
+ var destX = offsetX + col * paddedGlyphWidth * scale;
+ var destY = offsetY + row * paddedGlyphHeight * scale;
+ var destRect = new Rectangle(destX, destY, glyphWidth * scale, glyphHeight * scale);
+ var origin = new Vector2(0, 0);
+ Raylib.DrawTexturePro(atlasTexture.Texture, srcRect, destRect, origin, 0f, tint.ToRayColor());
+ }
+ }
+
+ #endregion
+
+ #region Draw
+
+ ///
+ /// Draws the specified text using the atlas, fitting each character into a grid within the given rectangle.
+ /// Only characters present in the atlas will be drawn.
+ ///
+ /// The text string to render.
+ /// The rectangle area where the text will be drawn.
+ /// The color to apply to the rendered text.
+ ///
+ /// Glyphs may be distorted if the grid cell aspect ratio does not match the glyph aspect ratio.
+ /// Use for uniform scaling of glyphs.
+ ///
+ public void Draw(string text, Rect rect, ColorRgba color)
+ {
+ if (!IsGenerated) throw new InvalidOperationException("Atlas not generated.");
+
+ var chars = text.ToCharArray();
+ var charRects = rect.GetAlignedRectsGrid(new Grid(chars.Length, 1), new Size(0, 0));
+ if (charRects == null) return;
+ for (int i = 0; i < chars.Length; i++)
+ {
+ var c = chars[i];
+ if (!glyphUvRects.TryGetValue(c, out var uvRect)) continue;
+
+ var srcRect = GenerateSourceRectangle(uvRect);
+ var destRect = new Rectangle(charRects[i].TopLeft.X, charRects[i].TopLeft.Y, charRects[i].Size.Width, charRects[i].Size.Height);
+ var origin = new Vector2(0, 0);
+ float rotation = 0f;
+ Raylib.DrawTexturePro(atlasTexture.Texture, srcRect, destRect, origin, rotation, color.ToRayColor());
+ }
+ }
+
+ ///
+ /// Draws the specified text using the atlas,
+ /// fitting each character into a grid within the given rectangle,
+ /// and applies custom horizontal and vertical padding to each glyph.
+ /// Only characters present in the atlas will be drawn.
+ ///
+ /// The text string to render.
+ /// The rectangle area where the text will be drawn.
+ /// The color to apply to the rendered text.
+ /// Horizontal padding to apply to each glyph.
+ /// Vertical padding to apply to each glyph.
+ ///
+ /// AnchorPoint value where X is between 0 and 1, determining horizontal alignment (0 = left, 1 = right, 0.5 = center),
+ /// and Y is between 0 and 1 for vertical alignment (0 = top, 1 = bottom, 0.5 = center).
+ ///
+ public void Draw(string text, Rect rect, ColorRgba color, float paddingX, float paddingY, AnchorPoint alignment)
+ {
+ if (!IsGenerated) throw new InvalidOperationException("Atlas not generated.");
+
+ var chars = text.ToCharArray();
+ float paddedGlyphWidth = glyphWidth + 2 * paddingX;
+ float paddedGlyphHeight = glyphHeight + 2 * paddingY;
+
+ // Calculate scale to fit all glyphs with padding into the rect
+ float scaleX = rect.Size.Width / (chars.Length * paddedGlyphWidth);
+ float scaleY = rect.Size.Height / paddedGlyphHeight;
+ float scale = Math.Min(scaleX, scaleY);
+
+ float totalWidth = chars.Length * paddedGlyphWidth * scale;
+ float totalHeight = paddedGlyphHeight * scale;
+
+ float offsetX = rect.TopLeft.X + (rect.Size.Width - totalWidth) * alignment.X;
+ float offsetY = rect.TopLeft.Y + (rect.Size.Height - totalHeight) * alignment.Y;
+
+ for (int i = 0; i < chars.Length; i++)
+ {
+ var c = chars[i];
+ if (!glyphUvRects.TryGetValue(c, out var uvRect)) continue;
+
+ var srcRect = GenerateSourceRectangle(uvRect);
+ var destX = offsetX + i * paddedGlyphWidth * scale + paddingX * scale;
+ var destY = offsetY + paddingY * scale;
+ var destRect = new Rectangle(destX, destY, glyphWidth * scale, glyphHeight * scale);
+ var origin = new Vector2(0, 0);
+ Raylib.DrawTexturePro(atlasTexture.Texture, srcRect, destRect, origin, 0f, color.ToRayColor());
+ }
+ }
+
+ ///
+ /// Draws the specified text using the atlas, scaling each glyph uniformly to fit within the given rectangle,
+ /// and wraps text to new lines as needed. Optionally prevents splitting words across lines.
+ /// The text is centered and scaled to maintain the glyph aspect ratio.
+ /// Only characters present in the atlas will be drawn.
+ ///
+ /// The text string to render.
+ /// The rectangle area where the text will be drawn.
+ /// The color to apply to the rendered text.
+ /// Absolute horizontal padding to apply to each glyph.
+ /// Absolute vertical padding to apply to each glyph.
+ /// If true, words will not be split across lines when wrapping.
+ public void DrawWithLineWrap(string text, Rect rect, ColorRgba color, float paddingX = 0f, float paddingY = 0f, bool dontSplitWords = false)
+ {
+ if (dontSplitWords)
+ {
+ DrawWithLineWrapNoWordSplit(text, rect, color, paddingX, paddingY);
+ return;
+ }
+
+ if (!IsGenerated) throw new InvalidOperationException("Atlas not generated.");
+
+ var chars = text.ToCharArray();
+ float paddedGlyphWidth = glyphWidth + 2 * paddingX;
+ float paddedGlyphHeight = glyphHeight + 2 * paddingY;
+
+ int glyphsPerLine = Math.Max(1, (int)Math.Ceiling(Math.Sqrt(chars.Length * rect.Size.Width / rect.Size.Height)));
+ int lines = (int)Math.Ceiling((double)chars.Length / glyphsPerLine);
+
+ float scaleX = rect.Size.Width / (glyphsPerLine * paddedGlyphWidth);
+ float scaleY = rect.Size.Height / (lines * paddedGlyphHeight);
+ float scale = Math.Min(scaleX, scaleY);
+
+ float totalWidth = glyphsPerLine * paddedGlyphWidth * scale;
+ float totalHeight = lines * paddedGlyphHeight * scale;
+ float offsetX = rect.TopLeft.X + (rect.Size.Width - totalWidth) / 2f;
+ float offsetY = rect.TopLeft.Y + (rect.Size.Height - totalHeight) / 2f;
+
+ int i = 0;
+ for (int line = 0; line < lines; line++)
+ {
+ for (int col = 0; col < glyphsPerLine && i < chars.Length; col++, i++)
+ {
+ var c = chars[i];
+ if (!glyphUvRects.TryGetValue(c, out var uvRect)) continue;
+
+ var srcRect = GenerateSourceRectangle(uvRect);
+ var destX = offsetX + col * paddedGlyphWidth * scale + paddingX * scale;
+ var destY = offsetY + line * paddedGlyphHeight * scale + paddingY * scale;
+ var destRect = new Rectangle(destX, destY, glyphWidth * scale, glyphHeight * scale);
+ var origin = new Vector2(0, 0);
+ Raylib.DrawTexturePro(atlasTexture.Texture, srcRect, destRect, origin, 0f, color.ToRayColor());
+ }
+ }
+ }
+
+ ///
+ /// Draws text using the atlas with uniform scaling and line wrapping, ensuring words are not split across lines.
+ /// Each line is built to fit within the available width, and words are wrapped to the next line if necessary.
+ ///
+ /// The text to draw.
+ /// The rectangle area to fit the text into.
+ /// The color to apply to the glyphs.
+ /// Horizontal padding for each glyph.
+ /// Vertical padding for each glyph.
+ private void DrawWithLineWrapNoWordSplit(string text, Rect rect, ColorRgba color, float paddingX = 0f, float paddingY = 0f)
+ {
+ if (!IsGenerated) throw new InvalidOperationException("Atlas not generated.");
+
+ var words = text.Split(' ');
+ float paddedGlyphWidth = glyphWidth + 2 * paddingX;
+ float paddedGlyphHeight = glyphHeight + 2 * paddingY;
+
+ // Estimate max glyphs per line
+ int maxGlyphsPerLine = Math.Max(1, (int)Math.Ceiling(Math.Sqrt(text.Length * rect.Size.Width / rect.Size.Height)));
+
+ // Build lines without splitting words
+ var linesList = new List();
+ string currentLine = "";
+ int currentLineLen = 0;
+ foreach (var word in words)
+ {
+ int wordLen = word.Length + (currentLineLen > 0 ? 1 : 0); // +1 for space
+ if (currentLineLen + wordLen > maxGlyphsPerLine && currentLineLen > 0)
+ {
+ linesList.Add(currentLine);
+ currentLine = word;
+ currentLineLen = word.Length;
+ }
+ else
+ {
+ if (currentLineLen > 0)
+ {
+ currentLine += " ";
+ currentLineLen++;
+ }
+ currentLine += word;
+ currentLineLen += word.Length;
+ }
+ }
+ if (currentLineLen > 0) linesList.Add(currentLine);
+
+ int lines = linesList.Count;
+ int glyphsPerLine = linesList.Max(l => l.Length);
+
+ float scaleX = rect.Size.Width / (glyphsPerLine * paddedGlyphWidth);
+ float scaleY = rect.Size.Height / (lines * paddedGlyphHeight);
+ float scale = Math.Min(scaleX, scaleY);
+
+ float totalWidth = glyphsPerLine * paddedGlyphWidth * scale;
+ float totalHeight = lines * paddedGlyphHeight * scale;
+ float offsetX = rect.TopLeft.X + (rect.Size.Width - totalWidth) / 2f;
+ float offsetY = rect.TopLeft.Y + (rect.Size.Height - totalHeight) / 2f;
+
+ for (int line = 0; line < lines; line++)
+ {
+ var chars = linesList[line].ToCharArray();
+ for (int col = 0; col < chars.Length; col++)
+ {
+ var c = chars[col];
+ if (!glyphUvRects.TryGetValue(c, out var uvRect)) continue;
+
+ var srcRect = GenerateSourceRectangle(uvRect);
+ var destX = offsetX + col * paddedGlyphWidth * scale + paddingX * scale;
+ var destY = offsetY + line * paddedGlyphHeight * scale + paddingY * scale;
+ var destRect = new Rectangle(destX, destY, glyphWidth * scale, glyphHeight * scale);
+ var origin = new Vector2(0, 0);
+ Raylib.DrawTexturePro(atlasTexture.Texture, srcRect, destRect, origin, 0f, color.ToRayColor());
+ }
+ }
+ }
+
+ #endregion
+
+ #region Helper
+
+ ///
+ /// Unloads the atlas texture from memory and marks the atlas as not generated.
+ /// Safe to call multiple times; does nothing if the atlas is not generated.
+ ///
+ public void Unload()
+ {
+ if (!IsGenerated) return;
+ Raylib.UnloadRenderTexture(atlasTexture);
+ IsGenerated = false;
+ }
+
+ ///
+ /// Calculates the optimal grid dimensions (rows and columns) for arranging a given number of glyphs.
+ /// The goal is to make the grid as square as possible, minimizing dead space and the difference between rows and columns.
+ ///
+ /// The total number of glyphs to arrange in the grid.
+ /// A tuple containing the number of rows and columns for the grid.
+ private static (int rows, int cols) CalculateGrid(int count)
+ {
+ // Try to make the grid as square as possible, minimizing dead space
+ int bestRows = 1, bestCols = count;
+ int minDeadSpace = int.MaxValue;
+ int minDiff = int.MaxValue;
+ for (int rows = 1; rows <= count; rows++)
+ {
+ int cols = (int)Math.Ceiling(count / (float)rows);
+ int deadSpace = rows * cols - count;
+ int diff = Math.Abs(rows - cols);
+ if (deadSpace < minDeadSpace || (deadSpace == minDeadSpace && diff < minDiff))
+ {
+ minDeadSpace = deadSpace;
+ minDiff = diff;
+ bestRows = rows;
+ bestCols = cols;
+ }
+ }
+ return (bestRows, bestCols);
+ }
+
+ ///
+ /// Generates a source rectangle for a glyph in the atlas texture, flipping it vertically
+ /// to match the expected orientation for rendering with Raylib.
+ ///
+ /// The UV rectangle of the glyph within the atlas.
+ /// A Rectangle representing the source area in the atlas texture.
+ ///
+ /// The source area has to be flipped on the atlas (top-left is actually bottom-left),
+ /// and the area itself has to be flipped vertically (-height).
+ ///
+ private Rectangle GenerateSourceRectangle(Rect uvRect)
+ {
+ return new Rectangle(uvRect.TopLeft.X, atlasHeight - uvRect.TopLeft.Y - uvRect.Size.Height, uvRect.Size.Width, -uvRect.Size.Height);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ShapeEngine/Text/BitmapFontStatic.cs b/ShapeEngine/Text/BitmapFontStatic.cs
new file mode 100644
index 00000000..b17b7bec
--- /dev/null
+++ b/ShapeEngine/Text/BitmapFontStatic.cs
@@ -0,0 +1,294 @@
+namespace ShapeEngine.Text;
+
+public partial class BitmapFont
+{
+ ///
+ /// The default 3x5 bitmap font. Each character is represented by a 3x5 grid using string arrays.
+ ///
+ public static readonly BitmapFont Default3x5Font =
+ new(
+ new Dictionary
+ {
+ { '0', new[] { "111", "101", "101", "101", "111" } },
+ { '1', new[] { "010", "010", "010", "010", "010" } },
+ { '2', new[] { "111", "001", "111", "100", "111" } },
+ { '3', new[] { "111", "001", "111", "001", "111" } },
+ { '4', new[] { "101", "101", "111", "001", "001" } },
+ { '5', new[] { "111", "100", "111", "001", "111" } },
+ { '6', new[] { "111", "100", "111", "101", "111" } },
+ { '7', new[] { "111", "001", "001", "001", "001" } },
+ { '8', new[] { "111", "101", "111", "101", "111" } },
+ { '9', new[] { "111", "101", "111", "001", "111" } },
+ // Uppercase letters
+ { 'A', new[] { "111", "101", "111", "101", "101" } },
+ { 'B', new[] { "110", "101", "110", "101", "110" } },
+ { 'C', new[] { "111", "100", "100", "100", "111" } },
+ { 'D', new[] { "110", "101", "101", "101", "110" } },
+ { 'E', new[] { "111", "100", "111", "100", "111" } },
+ { 'F', new[] { "111", "100", "111", "100", "100" } },
+ { 'G', new[] { "111", "100", "101", "101", "111" } },
+ { 'H', new[] { "101", "101", "111", "101", "101" } },
+ { 'I', new[] { "111", "010", "010", "010", "111" } },
+ { 'J', new[] { "001", "001", "001", "101", "111" } },
+ { 'K', new[] { "101", "101", "110", "101", "101" } },
+ { 'L', new[] { "100", "100", "100", "100", "111" } },
+ { 'M', new[] { "101", "111", "101", "101", "101" } },
+ { 'N', new[] { "101", "111", "111", "111", "101" } },
+ { 'O', new[] { "111", "101", "101", "101", "111" } },
+ { 'P', new[] { "111", "101", "111", "100", "100" } },
+ { 'Q', new[] { "111", "101", "101", "111", "011" } },
+ { 'R', new[] { "111", "101", "111", "101", "101" } },
+ { 'S', new[] { "111", "100", "111", "001", "111" } },
+ { 'T', new[] { "111", "010", "010", "010", "010" } },
+ { 'U', new[] { "101", "101", "101", "101", "111" } },
+ { 'V', new[] { "101", "101", "101", "101", "010" } },
+ { 'W', new[] { "101", "101", "101", "111", "101" } },
+ { 'X', new[] { "101", "101", "010", "101", "101" } },
+ { 'Y', new[] { "101", "101", "010", "010", "010" } },
+ { 'Z', new[] { "111", "001", "010", "100", "111" } },
+ // Lowercase letters (simple forms)
+ { 'a', new[] { "000", "011", "101", "111", "101" } },
+ { 'b', new[] { "100", "110", "101", "101", "110" } },
+ { 'c', new[] { "000", "011", "100", "100", "011" } },
+ { 'd', new[] { "001", "011", "101", "101", "011" } },
+ { 'e', new[] { "000", "011", "111", "100", "011" } },
+ { 'f', new[] { "011", "100", "111", "100", "100" } },
+ { 'g', new[] { "000", "011", "101", "011", "001" } },
+ { 'h', new[] { "100", "110", "101", "101", "101" } },
+ { 'i', new[] { "010", "000", "010", "010", "010" } },
+ { 'j', new[] { "001", "000", "001", "101", "010" } },
+ { 'k', new[] { "100", "101", "110", "101", "101" } },
+ { 'l', new[] { "010", "010", "010", "010", "010" } },
+ { 'm', new[] { "000", "110", "111", "101", "101" } },
+ { 'n', new[] { "000", "110", "101", "101", "101" } },
+ { 'o', new[] { "000", "011", "101", "101", "011" } },
+ { 'p', new[] { "000", "110", "101", "110", "100" } },
+ { 'q', new[] { "000", "011", "101", "011", "001" } },
+ { 'r', new[] { "000", "110", "101", "100", "100" } },
+ { 's', new[] { "000", "011", "110", "001", "110" } },
+ { 't', new[] { "010", "111", "010", "010", "011" } },
+ { 'u', new[] { "000", "101", "101", "101", "011" } },
+ { 'v', new[] { "000", "101", "101", "101", "010" } },
+ { 'w', new[] { "000", "101", "101", "111", "101" } },
+ { 'x', new[] { "000", "101", "010", "010", "101" } },
+ { 'y', new[] { "000", "101", "101", "011", "001" } },
+ { 'z', new[] { "000", "111", "010", "100", "111" } },
+ // Common special characters
+ { ' ', new[] { "000", "000", "000", "000", "000" } },
+ { '!', new[] { "010", "010", "010", "000", "010" } },
+ { '?', new[] { "111", "001", "010", "000", "010" } },
+ { '.', new[] { "000", "000", "000", "000", "010" } },
+ { ',', new[] { "000", "000", "000", "010", "100" } },
+ { '-', new[] { "000", "000", "111", "000", "000" } },
+ { '_', new[] { "000", "000", "000", "000", "111" } },
+ { ':', new[] { "000", "010", "000", "010", "000" } },
+ { ';', new[] { "000", "010", "000", "010", "100" } },
+ { '\'', new[] { "010", "010", "000", "000", "000" } },
+ { '"', new[] { "101", "101", "000", "000", "000" } },
+ { '(', new[] { "001", "010", "010", "010", "001" } },
+ { ')', new[] { "100", "010", "010", "010", "100" } },
+ { '[', new[] { "011", "010", "010", "010", "011" } },
+ { ']', new[] { "110", "010", "010", "010", "110" } },
+ { '/', new[] { "001", "001", "010", "100", "100" } },
+ { '\\', new[] { "100", "100", "010", "001", "001" } },
+ { '+', new[] { "000", "010", "111", "010", "000" } },
+ { '*', new[] { "010", "111", "010", "111", "010" } },
+ { '=', new[] { "000", "111", "000", "111", "000" } },
+ // Add more as needed
+ }
+ );
+
+ ///
+ /// The default 5x7 bitmap font. Each character is represented by a 5x7 grid using string arrays.
+ ///
+ public static readonly BitmapFont Default5x7Font =
+ new(
+ new Dictionary
+ {
+ // Digits
+ { '0', new[] { "01110", "10001", "10011", "10101", "11001", "10001", "01110" } },
+ { '1', new[] { "00100", "01100", "00100", "00100", "00100", "00100", "01110" } },
+ { '2', new[] { "01110", "10001", "00001", "00010", "00100", "01000", "11111" } },
+ { '3', new[] { "01110", "10001", "00001", "00110", "00001", "10001", "01110" } },
+ { '4', new[] { "00010", "00110", "01010", "10010", "11111", "00010", "00010" } },
+ { '5', new[] { "11111", "10000", "11110", "00001", "00001", "10001", "01110" } },
+ { '6', new[] { "00110", "01000", "10000", "11110", "10001", "10001", "01110" } },
+ { '7', new[] { "11111", "00001", "00010", "00100", "01000", "01000", "01000" } },
+ { '8', new[] { "01110", "10001", "10001", "01110", "10001", "10001", "01110" } },
+ { '9', new[] { "01110", "10001", "10001", "01111", "00001", "00010", "01100" } },
+ // Uppercase
+ { 'A', new[] { "01110", "10001", "10001", "11111", "10001", "10001", "10001" } },
+ { 'B', new[] { "11110", "10001", "10001", "11110", "10001", "10001", "11110" } },
+ { 'C', new[] { "01110", "10001", "10000", "10000", "10000", "10001", "01110" } },
+ { 'D', new[] { "11110", "10001", "10001", "10001", "10001", "10001", "11110" } },
+ { 'E', new[] { "11111", "10000", "10000", "11110", "10000", "10000", "11111" } },
+ { 'F', new[] { "11111", "10000", "10000", "11110", "10000", "10000", "10000" } },
+ { 'G', new[] { "01110", "10001", "10000", "10111", "10001", "10001", "01110" } },
+ { 'H', new[] { "10001", "10001", "10001", "11111", "10001", "10001", "10001" } },
+ { 'I', new[] { "01110", "00100", "00100", "00100", "00100", "00100", "01110" } },
+ { 'J', new[] { "00111", "00010", "00010", "00010", "10010", "10010", "01100" } },
+ { 'K', new[] { "10001", "10010", "10100", "11000", "10100", "10010", "10001" } },
+ { 'L', new[] { "10000", "10000", "10000", "10000", "10000", "10000", "11111" } },
+ { 'M', new[] { "10001", "11011", "10101", "10101", "10001", "10001", "10001" } },
+ { 'N', new[] { "10001", "10001", "11001", "10101", "10011", "10001", "10001" } },
+ { 'O', new[] { "01110", "10001", "10001", "10001", "10001", "10001", "01110" } },
+ { 'P', new[] { "11110", "10001", "10001", "11110", "10000", "10000", "10000" } },
+ { 'Q', new[] { "01110", "10001", "10001", "10001", "10101", "10010", "01101" } },
+ { 'R', new[] { "11110", "10001", "10001", "11110", "10100", "10010", "10001" } },
+ { 'S', new[] { "01111", "10000", "10000", "01110", "00001", "00001", "11110" } },
+ { 'T', new[] { "11111", "00100", "00100", "00100", "00100", "00100", "00100" } },
+ { 'U', new[] { "10001", "10001", "10001", "10001", "10001", "10001", "01110" } },
+ { 'V', new[] { "10001", "10001", "10001", "10001", "10001", "01010", "00100" } },
+ { 'W', new[] { "10001", "10001", "10001", "10101", "10101", "10101", "01010" } },
+ { 'X', new[] { "10001", "10001", "01010", "00100", "01010", "10001", "10001" } },
+ { 'Y', new[] { "10001", "10001", "01010", "00100", "00100", "00100", "00100" } },
+ { 'Z', new[] { "11111", "00001", "00010", "00100", "01000", "10000", "11111" } },
+ // Lowercase
+ { 'a', new[] { "00000", "00000", "01110", "00001", "01111", "10001", "01111" } },
+ { 'b', new[] { "10000", "10000", "10110", "11001", "10001", "10001", "11110" } },
+ { 'c', new[] { "00000", "00000", "01110", "10001", "10000", "10001", "01110" } },
+ { 'd', new[] { "00001", "00001", "01101", "10011", "10001", "10001", "01111" } },
+ { 'e', new[] { "00000", "00000", "01110", "10001", "11111", "10000", "01110" } },
+ { 'f', new[] { "00110", "01001", "01000", "11100", "01000", "01000", "01000" } },
+ { 'g', new[] { "00000", "00000", "01111", "10001", "01111", "00001", "01110" } },
+ { 'h', new[] { "10000", "10000", "10110", "11001", "10001", "10001", "10001" } },
+ { 'i', new[] { "00100", "00000", "01100", "00100", "00100", "00100", "01110" } },
+ { 'j', new[] { "00010", "00000", "00110", "00010", "00010", "10010", "01100" } },
+ { 'k', new[] { "10000", "10000", "10010", "10100", "11000", "10100", "10010" } },
+ { 'l', new[] { "01100", "00100", "00100", "00100", "00100", "00100", "01110" } },
+ { 'm', new[] { "00000", "00000", "11010", "10101", "10101", "10001", "10001" } },
+ { 'n', new[] { "00000", "00000", "10110", "11001", "10001", "10001", "10001" } },
+ { 'o', new[] { "00000", "00000", "01110", "10001", "10001", "10001", "01110" } },
+ { 'p', new[] { "00000", "00000", "11110", "10001", "11110", "10000", "10000" } },
+ { 'q', new[] { "00000", "00000", "01111", "10001", "01111", "00001", "00001" } },
+ { 'r', new[] { "00000", "00000", "10110", "11001", "10000", "10000", "10000" } },
+ { 's', new[] { "00000", "00000", "01111", "10000", "01110", "00001", "11110" } },
+ { 't', new[] { "01000", "01000", "11100", "01000", "01000", "01001", "00110" } },
+ { 'u', new[] { "00000", "00000", "10001", "10001", "10001", "10011", "01101" } },
+ { 'v', new[] { "00000", "00000", "10001", "10001", "10001", "01010", "00100" } },
+ { 'w', new[] { "00000", "00000", "10001", "10001", "10101", "10101", "01010" } },
+ { 'x', new[] { "00000", "00000", "10001", "01010", "00100", "01010", "10001" } },
+ { 'y', new[] { "00000", "00000", "10001", "10001", "01111", "00001", "01110" } },
+ { 'z', new[] { "00000", "00000", "11111", "00010", "00100", "01000", "11111" } },
+ // Special characters
+ { ' ', new[] { "00000", "00000", "00000", "00000", "00000", "00000", "00000" } },
+ { '!', new[] { "00100", "00100", "00100", "00100", "00000", "00000", "00100" } },
+ { '?', new[] { "01110", "10001", "00001", "00010", "00100", "00000", "00100" } },
+ { '.', new[] { "00000", "00000", "00000", "00000", "00000", "00100", "00100" } },
+ { ',', new[] { "00000", "00000", "00000", "00000", "00100", "00100", "01000" } },
+ { '-', new[] { "00000", "00000", "00000", "11111", "00000", "00000", "00000" } },
+ { '_', new[] { "00000", "00000", "00000", "00000", "00000", "00000", "11111" } },
+ { ':', new[] { "00000", "00100", "00000", "00000", "00100", "00000", "00000" } },
+ { ';', new[] { "00000", "00100", "00000", "00000", "00100", "00100", "01000" } },
+ { '\'', new[] { "00100", "00100", "00000", "00000", "00000", "00000", "00000" } },
+ { '"', new[] { "01010", "01010", "00000", "00000", "00000", "00000", "00000" } },
+ { '(', new[] { "00010", "00100", "01000", "01000", "01000", "00100", "00010" } },
+ { ')', new[] { "01000", "00100", "00010", "00010", "00010", "00100", "01000" } },
+ { '[', new[] { "00110", "00100", "00100", "00100", "00100", "00100", "00110" } },
+ { ']', new[] { "01100", "00100", "00100", "00100", "00100", "00100", "01100" } },
+ { '/', new[] { "00001", "00010", "00100", "01000", "10000", "00000", "00000" } },
+ { '\\', new[] { "10000", "01000", "00100", "00010", "00001", "00000", "00000" } },
+ { '+', new[] { "00000", "00100", "00100", "11111", "00100", "00100", "00000" } },
+ { '*', new[] { "00000", "01010", "00100", "11111", "00100", "01010", "00000" } },
+ { '=', new[] { "00000", "11111", "00000", "11111", "00000", "00000", "00000" } },
+ // Add more as needed
+ }
+ );
+
+ ///
+ /// The default 8x8 bitmap font. Each character is represented by an 8x8 grid using string arrays.
+ ///
+ public static readonly BitmapFont Default8x8Font =
+ new(
+ new Dictionary
+ {
+ // Digits
+ { '0', new[] { "00111100", "01000010", "10000001", "10000011", "10000101", "10001001", "01010010", "00111100" } },
+ { '1', new[] { "00011000", "00111000", "00011000", "00011000", "00011000", "00011000", "00011000", "00111100" } },
+ { '2', new[] { "00111100", "01000010", "00000010", "00000100", "00001000", "00010000", "00100000", "01111110" } },
+ { '3', new[] { "00111100", "01000010", "00000010", "00011100", "00000010", "01000010", "00111100", "00000000" } },
+ { '4', new[] { "00000100", "00001100", "00010100", "00100100", "01000100", "01111110", "00000100", "00000100" } },
+ { '5', new[] { "01111110", "01000000", "01111100", "00000010", "00000010", "01000010", "00111100", "00000000" } },
+ { '6', new[] { "00111100", "01000000", "10000000", "11111100", "10000010", "10000010", "01000010", "00111100" } },
+ { '7', new[] { "01111110", "00000010", "00000100", "00001000", "00010000", "00100000", "00100000", "00100000" } },
+ { '8', new[] { "00111100", "01000010", "01000010", "00111100", "01000010", "01000010", "00111100", "00000000" } },
+ { '9', new[] { "00111100", "01000010", "01000010", "00111110", "00000010", "00000100", "00111000", "00000000" } },
+ // Uppercase (A-Z)
+ { 'A', new[] { "00011000", "00100100", "01000010", "01000010", "01111110", "01000010", "01000010", "01000010" } },
+ { 'B', new[] { "01111100", "01000010", "01000010", "01111100", "01000010", "01000010", "01000010", "01111100" } },
+ { 'C', new[] { "00111100", "01000010", "10000000", "10000000", "10000000", "01000010", "00111100", "00000000" } },
+ { 'D', new[] { "01111000", "01000100", "01000010", "01000010", "01000010", "01000100", "01111000", "00000000" } },
+ { 'E', new[] { "01111110", "01000000", "01000000", "01111100", "01000000", "01000000", "01111110", "00000000" } },
+ { 'F', new[] { "01111110", "01000000", "01000000", "01111100", "01000000", "01000000", "01000000", "00000000" } },
+ { 'G', new[] { "00111100", "01000010", "10000000", "10011110", "10000010", "01000010", "00111100", "00000000" } },
+ { 'H', new[] { "01000010", "01000010", "01000010", "01111110", "01000010", "01000010", "01000010", "00000000" } },
+ { 'I', new[] { "00111100", "00011000", "00011000", "00011000", "00011000", "00011000", "00111100", "00000000" } },
+ { 'J', new[] { "00011110", "00000100", "00000100", "00000100", "01000100", "01000100", "00111000", "00000000" } },
+ { 'K', new[] { "01000010", "01000100", "01001000", "01110000", "01001000", "01000100", "01000010", "00000000" } },
+ { 'L', new[] { "01000000", "01000000", "01000000", "01000000", "01000000", "01000000", "01111110", "00000000" } },
+ { 'M', new[] { "01000010", "01100110", "01011010", "01000010", "01000010", "01000010", "01000010", "00000000" } },
+ { 'N', new[] { "01000010", "01100010", "01010010", "01001010", "01000110", "01000010", "01000010", "00000000" } },
+ { 'O', new[] { "00111100", "01000010", "10000001", "10000001", "10000001", "01000010", "00111100", "00000000" } },
+ { 'P', new[] { "01111100", "01000010", "01000010", "01111100", "01000000", "01000000", "01000000", "00000000" } },
+ { 'Q', new[] { "00111100", "01000010", "10000001", "10000001", "10001001", "01000010", "00111101", "00000000" } },
+ { 'R', new[] { "01111100", "01000010", "01000010", "01111100", "01001000", "01000100", "01000010", "00000000" } },
+ { 'S', new[] { "00111110", "01000000", "01000000", "00111100", "00000010", "00000010", "01111100", "00000000" } },
+ { 'T', new[] { "01111110", "00011000", "00011000", "00011000", "00011000", "00011000", "00011000", "00000000" } },
+ { 'U', new[] { "01000010", "01000010", "01000010", "01000010", "01000010", "01000010", "00111100", "00000000" } },
+ { 'V', new[] { "01000010", "01000010", "01000010", "01000010", "01000010", "00100100", "00011000", "00000000" } },
+ { 'W', new[] { "01000010", "01000010", "01000010", "01000010", "01011010", "01100110", "01000010", "00000000" } },
+ { 'X', new[] { "01000010", "01000010", "00100100", "00011000", "00100100", "01000010", "01000010", "00000000" } },
+ { 'Y', new[] { "01000010", "01000010", "00100100", "00011000", "00011000", "00011000", "00011000", "00000000" } },
+ { 'Z', new[] { "01111110", "00000010", "00000100", "00001000", "00010000", "00100000", "01111110", "00000000" } },
+ // Lowercase (a-z)
+ { 'a', new[] { "00000000", "00000000", "00111000", "00000100", "00111100", "01000100", "00111100", "00000000" } },
+ { 'b', new[] { "01000000", "01000000", "01111000", "01000100", "01000100", "01000100", "01111000", "00000000" } },
+ { 'c', new[] { "00000000", "00000000", "00111100", "01000000", "01000000", "01000000", "00111100", "00000000" } },
+ { 'd', new[] { "00000100", "00000100", "00111100", "01000100", "01000100", "01000100", "00111100", "00000000" } },
+ { 'e', new[] { "00000000", "00000000", "00111000", "01000100", "01111100", "01000000", "00111100", "00000000" } },
+ { 'f', new[] { "00011000", "00100100", "00100000", "01110000", "00100000", "00100000", "00100000", "00000000" } },
+ { 'g', new[] { "00000000", "00000000", "00111100", "01000100", "00111100", "00000100", "00111000", "00000000" } },
+ { 'h', new[] { "01000000", "01000000", "01111000", "01000100", "01000100", "01000100", "01000100", "00000000" } },
+ { 'i', new[] { "00010000", "00000000", "00110000", "00010000", "00010000", "00010000", "00111000", "00000000" } },
+ { 'j', new[] { "00001000", "00000000", "00011000", "00001000", "00001000", "01001000", "00110000", "00000000" } },
+ { 'k', new[] { "01000000", "01000000", "01001000", "01010000", "01100000", "01010000", "01001000", "00000000" } },
+ { 'l', new[] { "00110000", "00010000", "00010000", "00010000", "00010000", "00010000", "00111000", "00000000" } },
+ { 'm', new[] { "00000000", "00000000", "01101100", "01010100", "01010100", "01000100", "01000100", "00000000" } },
+ { 'n', new[] { "00000000", "00000000", "01111000", "01000100", "01000100", "01000100", "01000100", "00000000" } },
+ { 'o', new[] { "00000000", "00000000", "00111000", "01000100", "01000100", "01000100", "00111000", "00000000" } },
+ { 'p', new[] { "00000000", "00000000", "01111000", "01000100", "01111000", "01000000", "01000000", "00000000" } },
+ { 'q', new[] { "00000000", "00000000", "00111100", "01000100", "01111100", "00000100", "00000100", "00000000" } },
+ { 'r', new[] { "00000000", "00000000", "01111000", "01000100", "01000000", "01000000", "01000000", "00000000" } },
+ { 's', new[] { "00000000", "00000000", "00111100", "01000000", "00111000", "00000100", "01111000", "00000000" } },
+ { 't', new[] { "00100000", "00100000", "01110000", "00100000", "00100000", "00100100", "00011000", "00000000" } },
+ { 'u', new[] { "00000000", "00000000", "01000100", "01000100", "01000100", "01001100", "00110100", "00000000" } },
+ { 'v', new[] { "00000000", "00000000", "01000100", "01000100", "01000100", "00101000", "00010000", "00000000" } },
+ { 'w', new[] { "00000000", "00000000", "01000100", "01000100", "01010100", "01010100", "00101000", "00000000" } },
+ { 'x', new[] { "00000000", "00000000", "01000100", "00101000", "00010000", "00101000", "01000100", "00000000" } },
+ { 'y', new[] { "00000000", "00000000", "01000100", "01000100", "00111100", "00000100", "00111000", "00000000" } },
+ { 'z', new[] { "00000000", "00000000", "01111100", "00001000", "00010000", "00100000", "01111100", "00000000" } },
+ // Special characters
+ { ' ', new[] { "00000000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000" } },
+ { '!', new[] { "00010000", "00010000", "00010000", "00010000", "00010000", "00000000", "00010000", "00000000" } },
+ { '?', new[] { "00111000", "01000100", "00000100", "00001000", "00010000", "00000000", "00010000", "00000000" } },
+ { '.', new[] { "00000000", "00000000", "00000000", "00000000", "00000000", "00010000", "00010000", "00000000" } },
+ { ',', new[] { "00000000", "00000000", "00000000", "00000000", "00010000", "00010000", "00100000", "00000000" } },
+ { '-', new[] { "00000000", "00000000", "00000000", "01111110", "00000000", "00000000", "00000000", "00000000" } },
+ { '_', new[] { "00000000", "00000000", "00000000", "00000000", "00000000", "00000000", "01111110", "00000000" } },
+ { ':', new[] { "00000000", "00010000", "00000000", "00000000", "00010000", "00000000", "00000000", "00000000" } },
+ { ';', new[] { "00000000", "00010000", "00000000", "00000000", "00010000", "00010000", "00100000", "00000000" } },
+ { '\'', new[] { "00010000", "00010000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000" } },
+ { '"', new[] { "00101000", "00101000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000" } },
+ { '(', new[] { "00001000", "00010000", "00100000", "00100000", "00100000", "00010000", "00001000", "00000000" } },
+ { ')', new[] { "00100000", "00010000", "00001000", "00001000", "00001000", "00010000", "00100000", "00000000" } },
+ { '[', new[] { "00011000", "00010000", "00010000", "00010000", "00010000", "00010000", "00011000", "00000000" } },
+ { ']', new[] { "00110000", "00010000", "00010000", "00010000", "00010000", "00010000", "00110000", "00000000" } },
+ { '/', new[] { "00000010", "00000100", "00001000", "00010000", "00100000", "01000000", "00000000", "00000000" } },
+ { '\\', new[] { "01000000", "00100000", "00010000", "00001000", "00000100", "00000010", "00000000", "00000000" } },
+ { '+', new[] { "00000000", "00010000", "00010000", "01111110", "00010000", "00010000", "00000000", "00000000" } },
+ { '*', new[] { "00000000", "00101000", "00010000", "01111110", "00010000", "00101000", "00000000", "00000000" } },
+ { '=', new[] { "00000000", "01111110", "00000000", "01111110", "00000000", "00000000", "00000000", "00000000" } },
+ // Add more as needed
+ }
+ );
+}
\ No newline at end of file