using SFML.Graphics; using System; using System.Collections.Generic; using System.IO; using System.Text; namespace Otter { /// /// Graphic that renders text with some more options than normal Text. /// RichText can be very slow to render with large strings of text so be careful! /// For large blocks of text use the normal Text graphic. /// /// /// richText.String = "Hello, {color:f00}this text is red!{clear} {shake:4}Shaking text!"; /// /// Commands: /// {clear} - Clear all styles and reset back to normal, white text. /// {style:name} - Apply the style 'name' to text. Create styles with AddStyle(). /// {color:fff} - Colors text. Strings of 3, 4, 6, or 8 hex digits allowed. /// {color0:fff} - Colors the top left corner of characters. Strings of 3, 4, 6, or 8 hex digits allowed. /// {color1:fff} - Colors the top right corner of characters. Strings of 3, 4, 6, or 8 hex digits allowed. /// {color2:fff} - Colors the bottom right corner of characters. Strings of 3, 4, 6, or 8 hex digits allowed. /// {color3:fff} - Colors the bottom left corner of characters. Strings of 3, 4, 6, or 8 hex digits allowed. /// {colorShadow:fff} - Colors text shadow. Strings of 3, 4, 6, or 8 hex digits allowed. /// {colorOutline:fff} - Colors text outline. Strings of 3, 4, 6, or 8 hex digits allowed. /// {shadowX:0} - Set the drop shadow of the text on the X axis. /// {shadowY:0} - Set the drop shadow of the text on the Y axis. /// {shadow:0} - Set the drop shadow of the text on the X and Y axes. /// {outline:0} - Set the outline thickness on text. /// {shakeX:0} - Shake the text on the X axis with a float range. /// {shakeY:0} - Shake the text on the Y axis with a float range. /// {shake:0} - Shake the text on the X and Y axes with a float range. /// {waveAmpX:0} - Wave the text on the X axis with a float range. /// {waveAmpY:0} - Wave the text on the Y axis with a float range. /// {waveAmp:0} - Wave the text on the X and Y axes with a float range. /// {waveRateX:0} - Set the wave speed for the X axis. /// {waveRateY:0} - Set the wave speed for the Y axis. /// {waveRate:0} - Set the wave speed for the X and Y axes. /// {waveOffsetX:0} - Set the wave offset for the X axis. /// {waveOffsetY:0} - Set the wave offset for the Y axis. /// {waveOffset:0} - Set the wave offset for the X and Y axes. /// {offset:0} - Set the offset rate for characters. /// {charOffsetX:0} - Set the character offset X for the BitmapFont. /// {charOffsetY:0} - Set the character offset Y for the BitmapFont. /// /// public class RichText : Graphic { #region Static Fields static Dictionary styles = new Dictionary(); #endregion #region Static Methods /// /// Add a global style to RichText objects. The style will not be updated unless Refresh() is /// called on the objects. /// /// /// RichText.AddStyle("important","color:f00,waveAmpY:2,waveRate:2"); /// /// The name of the style. /// The properties to set using commas as a delim character. static public void AddStyle(string name, string content) { if (styles.ContainsKey(name)) { styles[name] = content; return; } styles.Add(name, content); } /// /// Removes a style from all RichText objects. /// /// The name of the style to remove. static public void RemoveStyle(string name) { styles.Remove(name); } /// /// Remove all styles from RichText objects. /// static public void ClearStyles() { styles.Clear(); } #endregion #region Private Fields List chars = new List(); List glyphs = new List(); Color currentCharColor = Color.White; Color currentCharColor0 = Color.White; Color currentCharColor1 = Color.White; Color currentCharColor2 = Color.White; Color currentCharColor3 = Color.White; Color currentShadowColor = Color.Black; Color currentOutlineColor = Color.White; float currentSineAmpX = 0; float currentSineAmpY = 0; float currentSineRateX = 1; float currentSineRateY = 1; float currentSineOffsetX = 0; float currentSineOffsetY = 0; float currentOffsetAmount = 10; float currentShadowX = 0; float currentShadowY = 0; float currentOutlineThickness = 0; int currentCharOffsetX = 0; int currentCharOffsetY = 0; float currentScaleX = 1; float currentScaleY = 1; float currentAngle = 0; bool currentBold = false; float currentShakeX = 0; float currentShakeY = 0; int totalHeight = 0; float timer = 0; int textWidth = -1; int textHeight = -1; bool wordWrap = false; float advanceSpace; string cachedCleanString = ""; string textString; string parsedString; List cachedLineWidths = new List(); BaseFont font; int charSize = 16; float boundsLeft; float boundsTop; #endregion #region Public Fields /// /// The alignment of the text. Left, Right, or Center. /// public TextAlign TextAlign = TextAlign.Left; /// /// The character used to mark an opening of a command. /// public char CommandOpen = '{'; /// /// The character used to mark the closing of a command. /// public char CommandClose = '}'; /// /// The character used to separate the command with the command value. /// public char CommandDelim = ':'; /// /// Controls the spacing between each character. If set above 0 the text will use a monospacing. /// public int MonospaceWidth = -1; /// /// The default horizontal amplitude of the sine wave. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultSineAmpX; /// /// The default vertical amplitude of the sine wave. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultSineAmpY; /// /// The default horizontal rate of the sine wave. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultSineRateX = 1; /// /// The default vertical rate of the sine wave. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultSineRateY = 1; /// /// The default horizontal offset of the sine wave. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultSineOffsetX; /// /// The default vertical offset of the sine wave. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultSineOffsetY; /// /// The default amount to offset each character for sine wave related transformations. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultOffsetAmount = 10; /// /// The default X position of the text shadow. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultShadowX; /// /// The default Y position of the text shadow. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultShadowY; /// /// The default outline thickness. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultOutlineThickness; /// /// The default horizontal shaking effect. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultShakeX; /// /// The default vertical shaking effect. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultShakeY; /// /// The default character color. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultCharColor = Color.White; /// /// The default color of the top left corner of each character. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultCharColor0 = Color.White; /// /// The default color of the top right corner of each character. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultCharColor1 = Color.White; /// /// The default color of the bottom right corner of each character. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultCharColor2 = Color.White; /// /// The default color of the bottom left corner of each character. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultCharColor3 = Color.White; /// /// The default shadow color. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultShadowColor = Color.Black; /// /// The default outline color. /// Will not take effect until the string changes, or Refresh() is called. /// public Color DefaultOutlineColor = Color.White; /// /// The default x scale of the characters. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultScaleX = 1; /// /// The default y scale of the characters. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultScaleY = 1; /// /// The default angle of the characters. /// Will not take effect until the string changes, or Refresh() is called. /// public float DefaultAngle = 0; /// /// The line height. 1 is 100% of the normal line height for the font. /// public float LineHeight = 1; /// /// The letter spacing. 1 is 100% of the normal letter spacing. /// public float LetterSpacing = 1; /// /// How far to offset the text rendering horizontally from the origin. /// public float OffsetX; /// /// How far to offset the text rendering vertically from the origin. /// public float OffsetY; /// /// The default config. /// public static RichTextConfig Default = new RichTextConfig(); #endregion #region Public Properties /// /// True if the text is using MonospaceWidth. /// public bool Monospaced { get { return MonospaceWidth > 0; } } /// /// The width of the text box. If not set it will be automatically set. /// public int TextWidth { get { return textWidth; } set { textWidth = value; UpdateCharacterData(); } } /// /// The height of the text box. If not set it will be automatically set. /// public int TextHeight { get { return textHeight; } set { textHeight = value; UpdateCharacterData(); } } /// /// The line spacing between each vertical line. /// public float LineSpacing { get { return font.GetLineSpacing(charSize); } } /// /// Determines if the text will automatically wrap. This will not work unless TextWidth is set. /// public bool WordWrap { get { return wordWrap; } set { wordWrap = value; UpdateCharacterData(); } } /// /// The font size of the text. /// public int FontSize { get { return charSize; } set { charSize = value; glyphs.Clear(); // Force update here UpdateDrawable(); } } /// /// True of the width was not manually set. /// public bool AutoWidth { get { return TextWidth < 0; } } /// /// True if the height was not manually set. /// public bool AutoHeight { get { return TextHeight < 0; } } /// /// The string to display stripped of all commands. /// public string CleanString { get { if (cachedCleanString != "") return cachedCleanString; var str = ""; foreach (var c in chars) { str += c.Character.ToString(); } cachedCleanString = str; return str; } } /// /// The pixel width of the longest line in the displayed string. /// public float LongestLine { get { var lines = CleanString.Split('\n'); float longest = 0; for (int i = 0; i < NumLines; i++) { var width = GetLineWidth(i); longest = Math.Max(longest, width); } return longest; } } /// /// The displayed string broken up into an array by lines. /// public string[] Lines { get; private set; } /// /// The total number of lines in the displayed string. /// public int NumLines { get { return Lines.Length; } } /// /// The string to display. This string can contain commands to alter the text dynamically. /// public string String { get { return textString; } set { textString = value; UpdateCharacterData(); // Force update here to set Width and Height and other stuff UpdateDrawable(); } } /// /// The character count of the string without formatting commands. /// public int CharacterCount { get { return chars.Count; } } /// /// The top bounds of the RichText. /// public float BoundsTop { get { return boundsTop; } } /// /// The top bounds of the RichText. /// public float BoundsLeft { get { return boundsLeft; } } #endregion #region Constructors /// /// Create a new RichText object. /// /// The string to display. This can include commands to alter text. /// The file path to the font to use. /// The font size to use. /// The width of the text box. /// The height of the text box. public RichText(string str, string font = "", int size = 16, int textWidth = -1, int textHeight = -1) : base() { Initialize(str, (font == "" ? new Font() : new Font(font)), size, textWidth, textHeight); } /// /// Create a new RichText object. /// /// The string to display. This can include commands to alter text. /// The stream of the font to use. /// The font size to use. /// The width of the text box. /// The height of the text box. public RichText(string str, Stream font, int size = 16, int textWidth = -1, int textHeight = -1) : base() { Initialize(str, new Font(font), size, textWidth, textHeight); } public RichText(string str, BaseFont font, int size = 16, int textWidth = -1, int textHeight = -1) : base() { Initialize(str, font, size, textWidth, textHeight); } /// /// Create a new RichText object using a RichTextConfig. /// /// The starting default text. /// The config to set all the default style values. /// The width of the text box. /// The height of the text box. public RichText(string str, RichTextConfig config, int textWidth = -1, int textHeight = -1) { // I probably should've used a dictionary of values or something. if (config == null) config = Default; DefaultSineAmpX = config.SineAmpX; DefaultSineAmpY = config.SineAmpY; DefaultSineRateX = config.SineRateX; DefaultSineRateY = config.SineRateY; DefaultSineOffsetX = config.SineOffsetX; DefaultSineOffsetY = config.SineOffsetY; DefaultOffsetAmount = config.OffsetAmount; DefaultShadowX = config.ShadowX; DefaultShadowY = config.ShadowY; DefaultOutlineThickness = config.OutlineThickness; DefaultShakeX = config.ShakeX; DefaultShakeY = config.ShakeY; DefaultCharColor = config.CharColor; DefaultCharColor0 = config.CharColor0; DefaultCharColor1 = config.CharColor1; DefaultCharColor2 = config.CharColor2; DefaultCharColor3 = config.CharColor3; DefaultShadowColor = config.ShadowColor; DefaultOutlineColor = config.OutlineColor; DefaultScaleX = config.ScaleX; DefaultScaleY = config.ScaleY; DefaultAngle = config.Angle; LineHeight = config.LineHeight; LetterSpacing = config.LetterSpacing; MonospaceWidth = config.MonospaceWidth; TextAlign = config.TextAlign; OffsetX = config.OffsetX; OffsetY = config.OffsetY; if (config.String != "") { str = config.String; } if (config.TextWidth != -1) { textWidth = config.TextWidth; } if (config.TextHeight != -1) { textHeight = config.TextHeight; } if (config.Font == null) { config.Font = new Font(); } Initialize(str, config.Font, config.FontSize, textWidth, textHeight); } /// /// Create a new RichText object. /// /// The string to display. /// The size of the font. public RichText(string str, int size) : this(str, "", size) { } /// /// Create a new RichText object. /// /// The size of the font. public RichText(int size = 16) : this("", "", size) { } /// /// Create a new RichText object /// /// The RichTextConfig to use. public RichText(RichTextConfig config) : this("", config) { } #endregion #region Private Methods void Initialize(string str, BaseFont font, int size, int textWidth, int textHeight) { Dynamic = true; this.font = font; charSize = size; TextWidth = textWidth; TextHeight = textHeight; String = str; roundRendering = false; if (font is BitmapFont) { charSize = 0; // Char size shouldn't matter for bitmap fonts? } } float Advance(Glyph glyph) { if (Monospaced) return MonospaceWidth; // Note: This is to fix an issue before SFML updates to 2.3!! var bytes = BitConverter.GetBytes(glyph.Advance); return BitConverter.ToSingle(bytes, 0); } Glyph Glyph(char charCode) { var g = font.GetGlyph(charCode, charSize, currentBold); //update otter texture because SFML font texture updates if (!glyphs.Contains(charCode)) { SetTexture(font.GetTexture(charSize)); glyphs.Add(charCode); } return g; } void PrecalculateLineWidths() { if (cachedLineWidths.Count > 0) return; // Already calculated, will reset when string changes. //Console.WriteLine("Precalculate line width go"); int lineNumber = 0; foreach (var line in Lines) { cachedLineWidths.Add(GetLineWidth(lineNumber)); lineNumber++; } //Console.WriteLine("cached line width count {0}", cachedLineWidths.Count); } void ApplyCommand(string command, string args) { switch (command) { case "color": currentCharColor = new Color(args); break; case "color0": currentCharColor0 = new Color(args); break; case "color1": currentCharColor1 = new Color(args); break; case "color2": currentCharColor2 = new Color(args); break; case "color3": currentCharColor3 = new Color(args); break; case "colorShadow": currentShadowColor = new Color(args); break; case "colorOutline": currentOutlineColor = new Color(args); break; case "outline": currentOutlineThickness = float.Parse(args); break; case "shakeX": currentShakeX = float.Parse(args); break; case "shakeY": currentShakeY = float.Parse(args); break; case "shake": currentShakeX = float.Parse(args); currentShakeY = float.Parse(args); break; case "waveAmpX": currentSineAmpX = float.Parse(args); break; case "waveAmpY": currentSineAmpY = float.Parse(args); break; case "waveAmp": currentSineAmpX = float.Parse(args); currentSineAmpY = float.Parse(args); break; case "waveRateX": currentSineRateX = float.Parse(args); break; case "waveRateY": currentSineRateY = float.Parse(args); break; case "waveRate": currentSineRateX = float.Parse(args); currentSineRateY = float.Parse(args); break; case "waveOffsetX": currentSineOffsetX = float.Parse(args); break; case "waveOffsetY": currentSineOffsetY = float.Parse(args); break; case "waveOffset": currentSineOffsetX = float.Parse(args); currentSineOffsetY = float.Parse(args); break; case "shadowX": currentShadowX = float.Parse(args); break; case "shadowY": currentShadowY = float.Parse(args); break; case "shadow": currentShadowX = float.Parse(args); currentShadowY = float.Parse(args); break; case "offset": currentOffsetAmount = float.Parse(args); break; case "bold": currentBold = int.Parse(args) > 0; break; case "charOffsetX": currentCharOffsetX = int.Parse(args); break; case "charOffsetY": currentCharOffsetY = int.Parse(args); break; case "scaleX": currentScaleX = float.Parse(args); break; case "scaleY": currentScaleY = float.Parse(args); break; case "angle": currentAngle = float.Parse(args); break; } } void UpdateCharacterData() { parsedString = textString; if (textString == "") { chars.Clear(); return; } var oldStringLength = chars.Count; cachedCleanString = ""; // Clear clean string cache cachedLineWidths.Clear(); Clear(); if (string.IsNullOrEmpty(textString)) textString = ""; var writingText = true; //auto word wrap string on input, before parsing? if (!AutoWidth && WordWrap) { textString = PreWrap(textString); } writingText = true; //create the set of chars with properties and parse commands var charIndex = 0; for (var i = 0; i < textString.Length; i++) { var c = textString[i]; if (c == CommandOpen) { //scan for commandclose var cmdEnd = textString.IndexOf(CommandClose, i + 1); if (cmdEnd >= 0) { //only continue of command close character is found writingText = false; var cmd = textString.Substring(i + 1, cmdEnd - i - 1); var cmdSplit = cmd.Split(CommandDelim); var command = cmdSplit[0]; if (command == "clear") { Clear(); } else if (command == "style") { var args = cmdSplit[1]; if (styles.ContainsKey(args)) { var stylestring = styles[args]; var styleSplit = stylestring.Split(','); foreach (var str in styleSplit) { var styleStrSplit = str.Split(CommandDelim); ApplyCommand(styleStrSplit[0], styleStrSplit[1]); } } } else { ApplyCommand(command, cmdSplit[1]); } continue; } } if (c == CommandClose) { writingText = true; continue; } if (writingText) { RichTextCharacter rtchar; if (chars.Count > charIndex) { rtchar = chars[charIndex]; rtchar.Character = c; } else { rtchar = new RichTextCharacter(c, charIndex); chars.Add(rtchar); } rtchar.Timer = timer; rtchar.sineAmpX = currentSineAmpX; rtchar.sineAmpY = currentSineAmpY; rtchar.sineRateX = currentSineRateX; rtchar.sineRateY = currentSineRateY; rtchar.sineOffsetX = currentSineOffsetX; rtchar.sineOffsetY = currentSineOffsetY; rtchar.offsetAmount = currentOffsetAmount; rtchar.shadowX = currentShadowX; rtchar.shadowY = currentShadowY; rtchar.shadowColor = currentShadowColor; rtchar.outlineThickness = currentOutlineThickness; rtchar.outlineColor = currentOutlineColor; rtchar.color = currentCharColor; rtchar.color0 = currentCharColor0; rtchar.color1 = currentCharColor1; rtchar.color2 = currentCharColor2; rtchar.color3 = currentCharColor3; rtchar.shakeX = currentShakeX; rtchar.shakeY = currentShakeY; rtchar.textureOffsetX = currentCharOffsetX; rtchar.textureOffsetY = currentCharOffsetY; rtchar.scaleX = currentScaleX; rtchar.scaleY = currentScaleY; rtchar.angle = currentAngle; charIndex++; } } if (charIndex < oldStringLength) { chars.RemoveRange(charIndex, oldStringLength - charIndex); } Lines = CleanString.Split('\n'); totalHeight = (int)Math.Ceiling(NumLines * font.GetLineSpacing(charSize) * LineHeight); } void Clear() { currentCharColor = DefaultCharColor; currentCharColor0 = DefaultCharColor0; currentCharColor1 = DefaultCharColor1; currentCharColor2 = DefaultCharColor2; currentCharColor3 = DefaultCharColor3; currentShadowColor = DefaultShadowColor; currentOutlineColor = DefaultOutlineColor; currentOutlineThickness = DefaultOutlineThickness; currentShadowX = DefaultShadowX; currentShadowY = DefaultShadowY; currentShakeX = DefaultShakeX; currentShakeY = DefaultShakeY; currentSineAmpX = DefaultSineAmpX; currentSineAmpY = DefaultSineAmpY; currentSineOffsetX = DefaultSineOffsetX; currentSineOffsetY = DefaultSineOffsetY; currentSineRateX = DefaultSineRateX; currentSineRateY = DefaultSineRateY; currentOffsetAmount = DefaultOffsetAmount; currentCharOffsetX = 0; currentCharOffsetY = 0; currentScaleX = 1; currentScaleY = 1; currentAngle = 0; } protected override void UpdateDrawable() { base.UpdateDrawable(); SFMLVertices.Clear(); // No precalculate for now, it just doubles the loops I think. //PrecalculateLineWidths(); advanceSpace = Advance(Glyph(' ')); //Figure out space ahead of time. float x = 0; float y = 0; int currentLine = 0; x = LineStartPosition(currentLine) + OffsetX; y = charSize + OffsetY; float lineLength = 0; float maxX = 0; float maxY = 0; float minY = charSize; float minX = charSize; var quadCount = 0; var buildLineCache = false; if (cachedLineWidths.Count == 0) { //build cached line widths this draw. //cache will be cleared on string change buildLineCache = true; } char prevChar = (char)0; for (var i = 0; i < chars.Count; i++) { var c = chars[i].Character; if (c == ' ' || c == '\t' || c == '\n') { minX = Util.Min(minX, x - LineStartPosition(currentLine)); minY = Util.Min(minY, y); switch (c) { case ' ': x += advanceSpace * LetterSpacing; lineLength += advanceSpace * LetterSpacing; break; case '\t': x += advanceSpace * 4 * LetterSpacing; lineLength += advanceSpace * 4 * LetterSpacing; break; case '\n': if (buildLineCache) cachedLineWidths.Add(lineLength); lineLength = 0; y += LineSpacing * LineHeight; currentLine++; x = LineStartPosition(currentLine); break; } maxX = Util.Max(maxX, x - LineStartPosition(currentLine)); maxY = Util.Max(maxY, y); } else { var glyph = Glyph(c); var rect = glyph.TextureRect; var bounds = glyph.Bounds; // Char offset only works with default formatted bitmap fonts!! rect.Top += chars[i].TextureOffsetY; rect.Left += chars[i].TextureOffsetX; // This is how you do kerning I guess x += font.GetKerning(prevChar, chars[i].Character, FontSize); var cx = chars[i].OffsetX; var cy = chars[i].OffsetY; var left = bounds.Left; var right = bounds.Left + bounds.Width; var top = bounds.Top; var bottom = bounds.Top + bounds.Height; var x1y1 = new Vector2(cx + x + left, cy + y + top); var x2y1 = new Vector2(cx + x + right, cy + y + top); var x2y2 = new Vector2(cx + x + right, cy + y + bottom); var x1y2 = new Vector2(cx + x + left, cy + y + bottom); var u1 = rect.Left; var v1 = rect.Top; var u2 = rect.Left + rect.Width; var v2 = rect.Top + rect.Height; //Console.WriteLine("{0}, {1}", c, rect); var charCenterX = cx + x + bounds.Left + bounds.Width / 2; var charCenterY = cy + y + bounds.Top + bounds.Height / 2; var charCenter = new Vector2(charCenterX, charCenterY); var scaleX = chars[i].ScaleX; var scaleY = chars[i].ScaleY; var angle = chars[i].Angle; // Scale the character verticies x1y1 = Util.ScaleAround(x1y1, charCenter, scaleX, scaleY); x1y2 = Util.ScaleAround(x1y2, charCenter, scaleX, scaleY); x2y2 = Util.ScaleAround(x2y2, charCenter, scaleX, scaleY); x2y1 = Util.ScaleAround(x2y1, charCenter, scaleX, scaleY); // Rotate the character verticies x1y1 = Util.RotateAround(x1y1, charCenter, angle); x1y2 = Util.RotateAround(x1y2, charCenter, angle); x2y2 = Util.RotateAround(x2y2, charCenter, angle); x2y1 = Util.RotateAround(x2y1, charCenter, angle); // Draw shadow Color nextColor; if (chars[i].ShadowX != 0 || chars[i].ShadowY != 0) { var shadowx = chars[i].ShadowX; var shadowy = chars[i].ShadowY; nextColor = chars[i].ShadowColor * Color; Append(shadowx + x1y1.X, shadowy + x1y1.Y, nextColor, u1, v1); Append(shadowx + x2y1.X, shadowy + x2y1.Y, nextColor, u2, v1); Append(shadowx + x2y2.X, shadowy + x2y2.Y, nextColor, u2, v2); Append(shadowx + x1y2.X, shadowy + x1y2.Y, nextColor, u1, v2); quadCount++; } // Draw outline if (chars[i].OutlineThickness > 0) { var outline = chars[i].OutlineThickness; nextColor = chars[i].OutlineColor * Color; for (float o = outline * 0.5f; o < outline; o += outline * 0.5f) { for (float r = 0; r < 360; r += 45) { var outlinex = Util.PolarX(r, o); var outliney = Util.PolarY(r, o); Append(outlinex + x1y1.X, outliney + x1y1.Y, nextColor, u1, v1); Append(outlinex + x2y1.X, outliney + x2y1.Y, nextColor, u2, v1); Append(outlinex + x2y2.X, outliney + x2y2.Y, nextColor, u2, v2); Append(outlinex + x1y2.X, outliney + x1y2.Y, nextColor, u1, v2); quadCount++; } } } // Draw character nextColor = chars[i].Color.Copy() * Color; nextColor *= chars[i].Color0; Append(x1y1.X, x1y1.Y, nextColor, u1, v1); nextColor = chars[i].Color.Copy() * Color; nextColor *= chars[i].Color1; Append(x2y1.X, x2y1.Y, nextColor, u2, v1); nextColor = chars[i].Color.Copy() * Color; nextColor *= chars[i].Color2; Append(x2y2.X, x2y2.Y, nextColor, u2, v2); nextColor = chars[i].Color.Copy() * Color; nextColor *= chars[i].Color3; Append(x1y2.X, x1y2.Y, nextColor, u1, v2); // Keep track of how many quads for debugging purposes quadCount++; // Update bounds. minX = Util.Min(minX, x + left - LineStartPosition(currentLine)); minY = Util.Min(minY, y + top); maxX = Util.Max(maxX, x + right - LineStartPosition(currentLine)); maxY = Util.Max(maxY, y + bottom); // Advance position x += Advance(glyph) * LetterSpacing; // Keep track of line length separately lineLength += Advance(glyph) * LetterSpacing; // Keep track of prev char for kernin' prevChar = chars[i].Character; } } // Handle Length of final line if (buildLineCache) cachedLineWidths.Add(lineLength); // Figure out dimensions if (AutoWidth) { Width = (int)(maxX - minX); } else { Width = TextWidth; } if (AutoHeight) { Height = (int)(maxY - minY); Height = (int)Util.Max(Height, charSize); // Temp fix for negative height? } else { Height = TextHeight; } boundsLeft = minX; boundsTop = minY; } float LineStartPosition(int lineNumber) { if (TextAlign == TextAlign.Left) return 0; float lineStart = 0; var lineLength = GetLineWidth(lineNumber); switch (TextAlign) { case TextAlign.Center: lineStart = (Width - lineLength) / 2; break; case TextAlign.Right: lineStart = Width - lineLength; break; } return lineStart; } #endregion #region Public Methods /// /// Center the RichText's origin. This factors in the RichText's local bounds. /// public void CenterTextOrigin() { CenterTextOriginX(); CenterTextOriginY(); } /// /// Center the RichText's Y origin. This factors in the RichText's top bounds. /// public void CenterTextOriginY() { OriginY = Util.Round(HalfHeight + BoundsTop); } /// /// Center the RichText's X origin. This factors in the RichText's left bounds. /// public void CenterTextOriginX() { OriginX = Util.Round(HalfWidth + BoundsLeft); } public override void CenterOrigin() { // Rounding to an int for this because origins of 0.5f cause bad text blurring. OriginX = Util.Round(HalfWidth); OriginY = Util.Round(HalfHeight); } /// /// Insert new lines into a string to prepare it for word wrapping with this object's width. /// This function will not wrap text if AutoWidth is true! /// /// The string to wrap. /// The wrapped string. public string PreWrap(string str) { if (AutoWidth) return str; //Auto width cannot auto word wrap. var finalStr = str; var writingText = true; float pixels = 0; int lastSpaceIndex = 0; for (var i = 0; i < str.Length; i++) { var c = str[i]; var glyph = Glyph(c); if (c == CommandOpen) { var cmdEnd = str.IndexOf(CommandClose, i + 1); if (cmdEnd >= 0) { writingText = false; } } if (!writingText) { if (c == CommandClose) { writingText = true; } } else if (writingText) { if (c == '\t') { pixels += Advance(glyph) * 4; } else if (c == '\n') { pixels = 0; } else { pixels += Advance(glyph); if (c == ' ') { // Keep track of the last space. lastSpaceIndex = i; } if (pixels > TextWidth) { StringBuilder sb = new StringBuilder(finalStr); // Turn last space into new line if pixels exceeds allowed width if (lastSpaceIndex < sb.Length) { sb[lastSpaceIndex] = '\n'; } finalStr = sb.ToString(); // Return the loop to the new line. i = lastSpaceIndex; // Reset the current pixel width. pixels = 0; } } } } return finalStr; } /// /// The line width in pixels of a specific line. /// /// The line number to check. /// The length of the line in pixels. public float GetLineWidth(int lineNumber) { if (lineNumber < 0 || lineNumber >= NumLines) throw new ArgumentOutOfRangeException("Line doesn't exist in string!"); if (lineNumber < cachedLineWidths.Count) return cachedLineWidths[lineNumber]; //This is very slow on large text objects, but I'm not sure how to get around that! var line = Lines[lineNumber]; float width = 0; foreach (var c in line) { var glyph = Glyph(c); if (c == '\t') { width += (Advance(glyph) * 3 * LetterSpacing); } width += (Advance(glyph) * LetterSpacing); } return width; } /// /// Refresh the text. This will reapply all commands and update the text image. /// public void Refresh() { UpdateCharacterData(); } /// /// Update the RichText. /// public override void Update() { timer += Game.Instance.DeltaTime; foreach (var c in chars) { c.Update(); } base.Update(); } /// /// Gets the font. /// /// The specific type of Font. /// The font as type font type T. public T GetFont() where T : BaseFont { return (T)font; } #endregion /// /// Retrieve the RichTextCharacter from the string. /// /// The index of the character. /// The RichTextCharacter at that index in the RichText string. public RichTextCharacter this[int index] { get { return chars[index]; } set { chars[index] = value; } } } #region Enum public enum TextAlign { Left, Right, Center } #endregion /// /// Internal class for managing characters in RichText. /// public class RichTextCharacter { #region Private Fields float finalShakeX; float finalShakeY; float finalSinX; float finalSinY; float activeScaleX = 1; float activeScaleY = 1; float activeAngle; float activeX; float activeY; float activeSineAmpX; float activeSineAmpY; float activeSineRateX; float activeSineRateY; float activeSineOffsetX; float activeSineOffsetY; float activeShakeX; float activeShakeY; float activeShadowX; float activeShadowY; float activeOutlineThickness; int activeTextureOffsetX; int activeTextureOffsetY; float activeOffsetAmount; Color activeColor = Color.White; Color activeColor0 = Color.White; Color activeColor1 = Color.White; Color activeColor2 = Color.White; Color activeColor3 = Color.White; Color activeShadowColor = Color.White; Color activeOutlineColor = Color.White; #endregion #region Public Fields /// /// The character. /// public char Character; /// /// Timer used for animation. /// public float Timer; /// /// The sine wave offset for this specific character. /// public float CharOffset; /// /// Determines if the character is bold. Not supported yet. /// public bool Bold = false; #endregion #region Public Properties /// /// The Color of the character. /// public Color Color { get { return color * activeColor; } set { activeColor = value; } } /// /// The Color of the top left corner. /// public Color Color0 { get { return color0 * activeColor0; } set { activeColor0 = value; } } /// /// The Color of the top left corner. /// public Color Color1 { get { return color1 * activeColor1; } set { activeColor1 = value; } } /// /// The Color of the top left corner. /// public Color Color2 { get { return color2 * activeColor2; } set { activeColor2 = value; } } /// /// The Color of the top left corner. /// public Color Color3 { get { return color3 * activeColor3; } set { activeColor3 = value; } } /// /// The Color of the shadow. /// public Color ShadowColor { get { return shadowColor * activeShadowColor; } set { activeShadowColor = value; } } /// /// The Color of the outline. /// public Color OutlineColor { get { return outlineColor * activeShadowColor; } set { activeShadowColor = value; } } /// /// The offset amount for each character. /// public float OffsetAmount { get { return offsetAmount + activeOffsetAmount; } set { activeOffsetAmount = value; } } /// /// The horizontal texture offset of the character. BitmapFont only. /// public int TextureOffsetX { get { return textureOffsetX + activeTextureOffsetX; } set { activeTextureOffsetX = value; } } /// /// The vertical texture offset of the character. BitmapFont only. /// public int TextureOffsetY { get { return textureOffsetY + activeTextureOffsetY; } set { activeTextureOffsetY = value; } } /// /// The outline thickness. /// public float OutlineThickness { get { return outlineThickness + activeOutlineThickness; } set { activeOutlineThickness = value; } } /// /// The horizontal sine wave offset. /// public float SineOffsetX { get { return sineOffsetX + activeSineOffsetX; } set { activeSineOffsetX = value; } } /// /// The vertical sine wave offset. /// public float SineOffsetY { get { return sineOffsetY + activeSineOffsetY; } set { activeSineOffsetY = value; } } /// /// The X position of the shadow. /// public float ShadowX { get { return shadowX + activeShadowX; } set { activeShadowX = value; } } /// /// The Y position of the shadow. /// public float ShadowY { get { return shadowY + activeShadowY; } set { activeShadowY = value; } } /// /// The horizontal sine wave rate. /// public float SineRateX { get { return sineRateX + activeSineRateX; } set { activeSineRateX = value; } } /// /// The vertical sine wave rate. /// public float SineRateY { get { return sineRateY + activeSineRateY; } set { activeSineRateY = value; } } /// /// The amount of horizontal shake. /// public float ShakeX { get { return shakeX + activeShakeX; } set { activeShakeX = value; } } /// /// The amount of vertical shake. /// public float ShakeY { get { return shakeY + activeShakeY; } set { activeShakeY = value; } } /// /// The horizontal sine wave amplitude. /// public float SineAmpX { get { return sineAmpX + activeSineAmpX; } set { activeSineAmpX = value; } } /// /// The vertical sine wave amplitude. /// public float SineAmpY { get { return sineAmpY + activeSineAmpY; } set { activeSineAmpY = value; } } /// /// The X scale of the character. /// public float ScaleX { get { return scaleX * activeScaleX; } set { activeScaleX = value; } } /// /// The Y scale of the character. /// public float ScaleY { get { return scaleY * activeScaleY; } set { activeScaleY = value; } } /// /// The angle of the character. /// public float Angle { get { return angle + activeAngle; } set { activeAngle = value; } } /// /// The X position offset of the character. /// public float X { get { return x + activeX; } set { activeX = value; } } /// /// The Y position offset of the character. /// public float Y { get { return y + activeY; } set { activeY = value; } } /// /// The final horizontal offset position of the character when rendered. /// public float OffsetX { get { return finalShakeX + finalSinX + X; } } /// /// The final vertical offset position of the character when rendered. /// public float OffsetY { get { return finalShakeY + finalSinY + Y; } } #endregion #region Constructors /// /// Creates a new RichTextCharacter. /// /// The character. /// The character offset for animation. public RichTextCharacter(char character, int charOffset = 0) { Character = character; CharOffset = charOffset; } #endregion #region Public Methods /// /// Update the character. /// public void Update() { Timer += Game.Instance.DeltaTime; finalShakeX = Rand.Float(-ShakeX, ShakeX); finalShakeY = Rand.Float(-ShakeY, ShakeY); finalSinX = Util.SinScale((Timer + SineOffsetX - CharOffset * OffsetAmount) * SineRateX, -SineAmpX, SineAmpX); finalSinY = Util.SinScale((Timer + SineOffsetY - CharOffset * OffsetAmount) * SineRateY, -SineAmpY, SineAmpY); } #endregion #region Internal internal void Append(VertexArray vertices, float x, float y) { var col = new Color(Color0); col.R *= Color.R; col.G *= Color.G; col.B *= Color.B; col.A *= Color.A; } internal float scaleX = 1; internal float scaleY = 1; internal float angle; internal float x; internal float y; internal float sineAmpX; internal float sineAmpY; internal float sineRateX = 1; internal float sineRateY = 1; internal float sineOffsetX; internal float sineOffsetY; internal float shakeX; internal float shakeY; internal float shadowX; internal float shadowY; internal float outlineThickness; internal int textureOffsetX; internal int textureOffsetY; internal float offsetAmount = 10; internal Color color = Color.White; internal Color color0 = Color.White; internal Color color1 = Color.White; internal Color color2 = Color.White; internal Color color3 = Color.White; internal Color shadowColor = Color.Black; internal Color outlineColor = Color.White; #endregion } /// /// A utility class used for storing default values for a RichText object. /// Set the values by using "var config = new RichTextConfig() { Font = "MyFont.ttf", FontSize = 16, ... };" /// public class RichTextConfig { #region Public Fields /// /// The horizontal sine wave amplitude. /// public float SineAmpX = 0; /// /// The vertical sine wave amplitude. /// public float SineAmpY = 0; /// /// The horizontal sine wave rate. /// public float SineRateX = 1; /// /// The vertical sine wave rate. /// public float SineRateY = 1; /// /// The horizontal sine wave offset. /// public float SineOffsetX = 0; /// /// The vertical sine wave offset. /// public float SineOffsetY = 0; /// /// The offset amount for each character for sine wave related transformations. /// public float OffsetAmount = 10; /// /// The X position of the shadow. /// public float ShadowX = 0; /// /// The Y position of the shadow. /// public float ShadowY = 0; /// /// The outline thickness. /// public float OutlineThickness = 0; /// /// The amount of horizontal shake. /// public float ShakeX = 0; /// /// The amount of vertical shake. /// public float ShakeY = 0; /// /// The Color of the character. /// public Color CharColor = Color.White; /// /// The Color of the top left corner. /// public Color CharColor0 = Color.White; /// /// The Color of the top left corner. /// public Color CharColor1 = Color.White; /// /// The Color of the top left corner. /// public Color CharColor2 = Color.White; /// /// The Color of the top left corner. /// public Color CharColor3 = Color.White; /// /// The Color of the shadow. /// public Color ShadowColor = Color.Black; /// /// The Color of the outline. /// public Color OutlineColor = Color.White; /// /// The X offset of the character. BitmapFont only. /// public int CharOffsetX = 0; /// /// The Y offset of the character. BitmapFont only. /// public int CharOffsetY = 0; /// /// The X scale of the character. /// public float ScaleX = 1; /// /// The Y scale of the character. /// public float ScaleY = 1; /// /// The angle of the character. /// public float Angle = 0; /// /// The spacing between each character. /// public float LetterSpacing = 1.0f; /// /// The line height between each line. Default is 1. /// public float LineHeight = 1.0f; /// /// Controls the spacing between each character. If set above 0 the text will use a monospacing. /// public int MonospaceWidth = -1; /// /// The alignment of the text. Left, Right, or Center. /// public TextAlign TextAlign = TextAlign.Left; /// /// The font to use. /// public BaseFont Font; /// /// The font size. /// public int FontSize = 16; /// /// The string to display. /// public string String = ""; /// /// The width of the text block. /// public int TextWidth = -1; /// /// The height of the text block. /// public int TextHeight = -1; /// /// How far to offset the text rendering horizontally from the origin. /// public float OffsetX; /// /// How far to offset the text rendering vertically from the origin. /// public float OffsetY; #endregion } }