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
}
}