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