diff --git a/osu.Framework.Font.Tests/Resources/Shaders/sh_Outline.fs b/osu.Framework.Font.Tests/Resources/Shaders/sh_Outline.fs new file mode 100644 index 0000000..0ad39fa --- /dev/null +++ b/osu.Framework.Font.Tests/Resources/Shaders/sh_Outline.fs @@ -0,0 +1,21 @@ +varying mediump vec2 v_TexCoord; +uniform lowp sampler2D m_Sampler; +uniform float g_outlineRadius; +uniform vec4 g_outlineColour; + +void main(void) +{ + vec4 col = texture2D(m_Sampler, v_TexCoord); + if (col.a > 0.5) + gl_FragColor = col; + else { + float a = texture2D(m_Sampler, vec2(v_TexCoord.x + g_outlineRadius, v_TexCoord.y)).a + + texture2D(m_Sampler, vec2(v_TexCoord.x, v_TexCoord.y - g_outlineRadius)).a + + texture2D(m_Sampler, vec2(v_TexCoord.x - g_outlineRadius, v_TexCoord.y)).a + + texture2D(m_Sampler, vec2(v_TexCoord.x, v_TexCoord.y + g_outlineRadius)).a; + if (col.a < 1.0 && a > 0.0) + gl_FragColor = g_outlineColour; + else + gl_FragColor = col; + } +} diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKarakeSpriteText.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKarakeSpriteText.cs index 2335e5e..1fa2d45 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKarakeSpriteText.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKarakeSpriteText.cs @@ -116,8 +116,11 @@ private Drawable createCustomizeTimeTagKaroakeText(Dictionary ru Margin = new MarginPadding(30), Shadow = true, ShadowOffset = new Vector2(3), + Outline = true, + OutlineRadius = 3f, + TextTexture = new SolidTexture { SolidColor = Color4.Blue }, ShadowTexture = new SolidTexture { SolidColor = Color4.Red }, - TextTexture = new SolidTexture { SolidColor = Color4.White } + BorderTexture = new SolidTexture { SolidColor = Color4.White } }; } } diff --git a/osu.Framework.Font.Tests/osu.Framework.Font.Tests.csproj b/osu.Framework.Font.Tests/osu.Framework.Font.Tests.csproj index 69977a4..7c7f66d 100644 --- a/osu.Framework.Font.Tests/osu.Framework.Font.Tests.csproj +++ b/osu.Framework.Font.Tests/osu.Framework.Font.Tests.csproj @@ -18,4 +18,7 @@ + + + \ No newline at end of file diff --git a/osu.Framework.Font/Graphics/Sprites/KarokeSpriteText.cs b/osu.Framework.Font/Graphics/Sprites/KarokeSpriteText.cs index d94a32f..7a6de78 100644 --- a/osu.Framework.Font/Graphics/Sprites/KarokeSpriteText.cs +++ b/osu.Framework.Font/Graphics/Sprites/KarokeSpriteText.cs @@ -262,23 +262,23 @@ public LyricTextAlignment RomajiAlignment } } - public float BorderRadius + public bool Outline { - get => frontLyricText.BorderRadius; + get => frontLyricText.Outline; set { - frontLyricText.BorderRadius = value; - backLyricText.BorderRadius = value; + frontLyricText.Outline = value; + backLyricText.Outline = value; } } - public bool Border + public float OutlineRadius { - get => frontLyricText.Border; + get => frontLyricText.OutlineRadius; set { - frontLyricText.Border = value; - backLyricText.Border = value; + frontLyricText.OutlineRadius = value; + backLyricText.OutlineRadius = value; } } diff --git a/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs b/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs index 6ce5acc..0865381 100644 --- a/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs +++ b/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs @@ -10,7 +10,7 @@ namespace osu.Framework.Graphics.Sprites { - public class LyricSpriteText : SpriteText + public class LyricSpriteText : StyledSpriteText { public LyricSpriteText() { @@ -124,6 +124,8 @@ public Vector2 RubySpacing } } + protected Vector2 CalculatedRubySpacing => Outline ? RubySpacing + new Vector2(OutlineRadius) : RubySpacing; + private Vector2 romajiSpacing; public Vector2 RomajiSpacing @@ -139,6 +141,8 @@ public Vector2 RomajiSpacing } } + protected Vector2 CalculatedRomajiSpacing => Outline ? RomajiSpacing + new Vector2(OutlineRadius) : RomajiSpacing; + private ILyricTexture textTexture; public ILyricTexture TextTexture @@ -182,6 +186,7 @@ public ILyricTexture BorderTexture return; borderTexture = value; + OutlineColour = (borderTexture as SolidTexture)?.SolidColor ?? Color4.White; Invalidate(Invalidation.All); } } @@ -216,36 +221,6 @@ public LyricTextAlignment RomajiAlignment } } - private float borderRadius; - - public float BorderRadius - { - get => borderRadius; - set - { - if (borderRadius == value) - return; - - borderRadius = value; - Invalidate(Invalidation.All); - } - } - - private bool border; - - public bool Border - { - get => border; - set - { - if (border == value) - return; - - border = value; - Invalidate(Invalidation.All); - } - } - public new Vector2 ShadowOffset { get => base.ShadowOffset * Font.Size; @@ -256,7 +231,7 @@ public bool Border protected TextBuilderGlyph[] Characters; /// - /// Creates a to generate the character layout for this . + /// Creates a to generate the character layout for this . /// /// The where characters should be retrieved from. /// The . @@ -271,20 +246,20 @@ protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store var contentPosition = rubyYPosition + RubyFont.Size / 2 + RubyMargin; // Print and save main texts - var charactersBacking = createMainTexts(Text,Font, contentPosition, Spacing); + var charactersBacking = createMainTexts(Text,Font, contentPosition, CalculatedSpacing); Characters = charactersBacking.ToArray(); // Print ruby texts - createTexts(Rubies, RubyFont, rubyYPosition, RubySpacing); + createTexts(Rubies, RubyFont, rubyYPosition, CalculatedRubySpacing); // Calculate position and print romaji texts var romajiYPosition = contentPosition + Characters.FirstOrDefault().Height + Characters.FirstOrDefault().YOffset + RomajiMargin; - createTexts(Romajies, RomajiFont, romajiYPosition, RomajiSpacing); + createTexts(Romajies, RomajiFont, romajiYPosition, CalculatedRomajiSpacing); // Calculate position and return TextBuilder that do not renderer text anymore var romajiTextSize = RomajiMargin + ((Romajies?.Any() ?? false) ? (charactersBacking.LastOrDefault().Height + charactersBacking.LastOrDefault().YOffset) : 0); return new TextBuilder(store, Font, builder_max_width, UseFullGlyphHeight, - new Vector2(Padding.Left, contentPosition + romajiTextSize), Spacing, null, + new Vector2(Padding.Left, contentPosition + romajiTextSize), CalculatedSpacing, null, excludeCharacters, FallbackCharacter); // Create main text diff --git a/osu.Framework.Font/Graphics/Sprites/StyledSpriteText.cs b/osu.Framework.Font/Graphics/Sprites/StyledSpriteText.cs new file mode 100644 index 0000000..04a2932 --- /dev/null +++ b/osu.Framework.Font/Graphics/Sprites/StyledSpriteText.cs @@ -0,0 +1,680 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Development; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.IO.Stores; +using osu.Framework.Layout; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osu.Framework.Text; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Graphics.Sprites +{ + /// + /// A container for simple text rendering purposes. If more complex text rendering is required, use instead. + /// + public partial class StyledSpriteText : Drawable, IHasLineBaseHeight, ITexturedShaderDrawable, IHasText, IHasFilterTerms, IFillFlowContainer, IHasCurrentValue + { + private const float default_text_size = 20; + private static readonly char[] default_never_fixed_width_characters = { '.', ',', ':', ' ' }; + + [Resolved] + private FontStore store { get; set; } + + [Resolved] + private LocalisationManager localisation { get; set; } + + private ILocalisedBindableString localisedText; + + public IShader TextureShader { get; private set; } + public IShader RoundedTextureShader { get; private set; } + + public StyledSpriteText() + { + current.BindValueChanged(text => Text = text.NewValue); + + AddLayout(charactersCache); + AddLayout(parentScreenSpaceCache); + AddLayout(localScreenSpaceCache); + AddLayout(shadowOffsetCache); + AddLayout(outlineOffsetCache); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + localisedText = localisation.GetLocalisedString(text); + localisedText.BindValueChanged(str => + { + if (string.IsNullOrEmpty(str.NewValue)) + { + // We'll become not present and won't update the characters to set the size to 0, so do it manually + if (requiresAutoSizedWidth) + base.Width = Padding.TotalHorizontal; + if (requiresAutoSizedHeight) + base.Height = Padding.TotalVertical; + } + + invalidate(true); + }, true); + + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "Outline"); + RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "Outline"); + + // Pre-cache the characters in the texture store + foreach (var character in displayedText) + { + var unused = store.Get(font.FontName, character) ?? store.Get(null, character); + } + } + + private LocalisedString text = string.Empty; + + /// + /// Gets or sets the text to be displayed. + /// + public LocalisedString Text + { + get => text; + set + { + if (text == value) + return; + + text = value; + + current.Value = text; + + if (localisedText != null) + localisedText.Text = value; + } + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private string displayedText => localisedText?.Value ?? text.Text.Original; + + string IHasText.Text + { + get => Text; + set => Text = value; + } + + private FontUsage font = FontUsage.Default; + + /// + /// Contains information on the font used to display the text. + /// + public FontUsage Font + { + get => font; + set + { + font = value; + + invalidate(true); + shadowOffsetCache.Invalidate(); + outlineOffsetCache.Invalidate(); + } + } + + private bool allowMultiline = true; + + /// + /// True if the text should be wrapped if it gets too wide. Note that \n does NOT cause a line break. If you need explicit line breaks, use instead. + /// + /// + /// If enabled, will be disabled. + /// + public bool AllowMultiline + { + get => allowMultiline; + set + { + if (allowMultiline == value) + return; + + if (value) + Truncate = false; + + allowMultiline = value; + + invalidate(true); + } + } + + private bool shadow; + + /// + /// True if a shadow should be displayed around the text. + /// + public bool Shadow + { + get => shadow; + set + { + if (shadow == value) + return; + + shadow = value; + + Invalidate(Invalidation.DrawNode); + } + } + + private Color4 shadowColour = new Color4(0, 0, 0, 0.2f); + + /// + /// The colour of the shadow displayed around the text. A shadow will only be displayed if the property is set to true. + /// + public Color4 ShadowColour + { + get => shadowColour; + set + { + if (shadowColour == value) + return; + + shadowColour = value; + + Invalidate(Invalidation.DrawNode); + } + } + + private Vector2 shadowOffset = new Vector2(0, 0.06f); + + /// + /// The offset of the shadow displayed around the text. A shadow will only be displayed if the property is set to true. + /// + public Vector2 ShadowOffset + { + get => shadowOffset; + set + { + if (shadowOffset == value) + return; + + shadowOffset = value; + + invalidate(true); + shadowOffsetCache.Invalidate(); + } + } + + private bool outline; + + /// + /// True if a outline should be displayed around the text. + /// + public bool Outline + { + get => outline; + set + { + if (outline == value) + return; + + outline = value; + + Invalidate(Invalidation.DrawNode); + } + } + + private Color4 outlineColour = new Color4(0, 0, 0, 0.2f); + + /// + /// The colour of the outline displayed around the text. A outline will only be displayed if the property is set to true. + /// + public Color4 OutlineColour + { + get => outlineColour; + set + { + if (outlineColour == value) + return; + + outlineColour = value; + + Invalidate(Invalidation.DrawNode); + } + } + + private float outlineRadius; + + /// + /// The offset of the outline displayed around the text. A outline will only be displayed if the property is set to true. + /// + public float OutlineRadius + { + get => outlineRadius; + set + { + if (outlineRadius == value) + return; + + outlineRadius = value; + + invalidate(true); + outlineOffsetCache.Invalidate(); + } + } + + private bool useFullGlyphHeight = true; + + /// + /// True if the 's vertical size should be equal to (the full height) or precisely the size of used characters. + /// Set to false to allow better centering of individual characters/numerals/etc. + /// + public bool UseFullGlyphHeight + { + get => useFullGlyphHeight; + set + { + if (useFullGlyphHeight == value) + return; + + useFullGlyphHeight = value; + + invalidate(true); + } + } + + private bool truncate; + + /// + /// If true, text should be truncated when it exceeds the of this . + /// + /// + /// Has no effect if no or custom sizing is set. + /// If enabled, will be disabled. + /// + public bool Truncate + { + get => truncate; + set + { + if (truncate == value) return; + + if (value) + AllowMultiline = false; + + truncate = value; + invalidate(true); + } + } + + private string ellipsisString = "…"; + + /// + /// When is enabled, this decides what string is used to signify that truncation has occured. + /// Defaults to "…". + /// + public string EllipsisString + { + get => ellipsisString; + set + { + if (ellipsisString == value) return; + + ellipsisString = value; + invalidate(true); + } + } + + private bool requiresAutoSizedWidth => explicitWidth == null && (RelativeSizeAxes & Axes.X) == 0; + + private bool requiresAutoSizedHeight => explicitHeight == null && (RelativeSizeAxes & Axes.Y) == 0; + + private float? explicitWidth; + + /// + /// Gets or sets the width of this . The will maintain this width when set. + /// + public override float Width + { + get + { + if (requiresAutoSizedWidth) + computeCharacters(); + return base.Width; + } + set + { + if (explicitWidth == value) + return; + + base.Width = value; + explicitWidth = value; + + invalidate(true); + } + } + + private float maxWidth = float.PositiveInfinity; + + /// + /// The maximum width of this . Affects both auto and fixed sizing modes. + /// + /// + /// This becomes a relative value if this is relatively-sized on the X-axis. + /// + public float MaxWidth + { + get => maxWidth; + set + { + if (maxWidth == value) + return; + + maxWidth = value; + invalidate(true); + } + } + + private float? explicitHeight; + + /// + /// Gets or sets the height of this . The will maintain this height when set. + /// + public override float Height + { + get + { + if (requiresAutoSizedHeight) + computeCharacters(); + return base.Height; + } + set + { + if (explicitHeight == value) + return; + + base.Height = value; + explicitHeight = value; + + invalidate(true); + } + } + + /// + /// Gets or sets the size of this . The will maintain this size when set. + /// + public override Vector2 Size + { + get + { + if (requiresAutoSizedWidth || requiresAutoSizedHeight) + computeCharacters(); + return base.Size; + } + set + { + Width = value.X; + Height = value.Y; + } + } + + private Vector2 spacing; + + /// + /// Gets or sets the spacing between characters of this . + /// + public Vector2 Spacing + { + get => spacing; + set + { + if (spacing == value) + return; + + spacing = value; + + invalidate(true); + } + } + + protected Vector2 CalculatedSpacing => Outline ? Spacing + new Vector2(OutlineRadius) : Spacing; + + private MarginPadding padding; + + /// + /// Shrinks the space which may be occupied by characters of this by the specified amount on each side. + /// + public MarginPadding Padding + { + get => padding; + set + { + if (padding.Equals(value)) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}."); + + padding = value; + + invalidate(true); + } + } + + public override bool IsPresent => base.IsPresent && (AlwaysPresent || !string.IsNullOrEmpty(displayedText)); + + #region Characters + + private readonly LayoutValue charactersCache = new LayoutValue(Invalidation.DrawSize | Invalidation.Presence, InvalidationSource.Parent); + private readonly List charactersBacking = new List(); + + /// + /// The characters in local space. + /// + private List characters + { + get + { + computeCharacters(); + return charactersBacking; + } + } + + private bool isComputingCharacters; + + /// + /// Compute character textures and positions. + /// + private void computeCharacters() + { + /* + if (LoadState >= LoadState.Loaded) + ThreadSafety.EnsureUpdateThread(); + */ + + if (store == null) + return; + + if (charactersCache.IsValid) + return; + + charactersBacking.Clear(); + + Debug.Assert(!isComputingCharacters, "Cyclic invocation of computeCharacters()!"); + isComputingCharacters = true; + + TextBuilder textBuilder = null; + + try + { + if (string.IsNullOrEmpty(displayedText)) + return; + + textBuilder = CreateTextBuilder(store); + textBuilder.AddText(displayedText); + } + finally + { + if (requiresAutoSizedWidth) + base.Width = (textBuilder?.Bounds.X ?? 0) + Padding.Right; + if (requiresAutoSizedHeight) + base.Height = (textBuilder?.Bounds.Y ?? 0) + Padding.Bottom; + + base.Width = Math.Min(base.Width, MaxWidth); + + isComputingCharacters = false; + charactersCache.Validate(); + } + } + + private readonly LayoutValue parentScreenSpaceCache = new LayoutValue(Invalidation.DrawSize | Invalidation.Presence | Invalidation.DrawInfo, InvalidationSource.Parent); + private readonly LayoutValue localScreenSpaceCache = new LayoutValue(Invalidation.MiscGeometry, InvalidationSource.Self); + + private readonly List screenSpaceCharactersBacking = new List(); + + /// + /// The characters in screen space. These are ready to be drawn. + /// + private List screenSpaceCharacters + { + get + { + computeScreenSpaceCharacters(); + return screenSpaceCharactersBacking; + } + } + + private void computeScreenSpaceCharacters() + { + if (!parentScreenSpaceCache.IsValid) + { + localScreenSpaceCache.Invalidate(); + parentScreenSpaceCache.Validate(); + } + + if (localScreenSpaceCache.IsValid) + return; + + screenSpaceCharactersBacking.Clear(); + + Vector2 inflationAmount = DrawInfo.MatrixInverse.ExtractScale().Xy; + + foreach (var character in characters) + { + screenSpaceCharactersBacking.Add(new ScreenSpaceCharacterPart + { + DrawQuad = ToScreenSpace(character.DrawRectangle.Inflate(inflationAmount)), + InflationPercentage = Vector2.Divide(inflationAmount, character.DrawRectangle.Size), + Texture = character.Texture + }); + } + + localScreenSpaceCache.Validate(); + } + + private readonly LayoutValue shadowOffsetCache = new LayoutValue(Invalidation.DrawInfo, InvalidationSource.Parent); + + private Vector2 premultipliedShadowOffset => + shadowOffsetCache.IsValid ? shadowOffsetCache.Value : shadowOffsetCache.Value = ToScreenSpace(shadowOffset * Font.Size) - ToScreenSpace(Vector2.Zero); + + private readonly LayoutValue outlineOffsetCache = new LayoutValue(Invalidation.DrawInfo, InvalidationSource.Parent); + + private Vector2 premultipliedOutlineOffset => + outlineOffsetCache.IsValid ? outlineOffsetCache.Value : outlineOffsetCache.Value = ToScreenSpace(shadowOffset * Font.Size) - ToScreenSpace(Vector2.Zero); + + #endregion + + #region Invalidation + + private void invalidate(bool layout = false) + { + if (layout) + charactersCache.Invalidate(); + parentScreenSpaceCache.Invalidate(); + localScreenSpaceCache.Invalidate(); + + Invalidate(Invalidation.DrawNode); + } + + #endregion + + #region DrawNode + + protected override DrawNode CreateDrawNode() => new SpriteTextDrawNode(this); + + #endregion + + /// + /// The characters that should be excluded from fixed-width application. Defaults to (".", ",", ":", " ") if null. + /// + protected virtual char[] FixedWidthExcludeCharacters { get; } = null; + + /// + /// The character to fallback to use if a character glyph lookup failed. + /// + protected virtual char FallbackCharacter => '?'; + + /// + /// Creates a to generate the character layout for this . + /// + /// The where characters should be retrieved from. + /// The . + protected virtual TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) + { + var excludeCharacters = FixedWidthExcludeCharacters ?? default_never_fixed_width_characters; + + float builderMaxWidth = requiresAutoSizedWidth + ? MaxWidth + : ApplyRelativeAxes(RelativeSizeAxes, new Vector2(Math.Min(MaxWidth, base.Width), base.Height), FillMode).X - Padding.Right; + + if (AllowMultiline) + { + return new MultilineTextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), CalculatedSpacing, charactersBacking, + excludeCharacters, FallbackCharacter); + } + + if (Truncate) + { + return new TruncatingTextBuilder(store, Font, builderMaxWidth, ellipsisString, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), CalculatedSpacing, charactersBacking, + excludeCharacters, FallbackCharacter); + } + + return new TextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), CalculatedSpacing, charactersBacking, + excludeCharacters, FallbackCharacter); + } + + public override string ToString() => $@"""{displayedText}"" " + base.ToString(); + + /// + /// Gets the base height of the font used by this text. If the font of this text is invalid, 0 is returned. + /// + public float LineBaseHeight + { + get + { + var baseHeight = store.GetBaseHeight(Font.FontName); + if (baseHeight.HasValue) + return baseHeight.Value * Font.Size; + + if (string.IsNullOrEmpty(displayedText)) + return 0; + + return store.GetBaseHeight(displayedText[0]).GetValueOrDefault() * Font.Size; + } + } + + public IEnumerable FilterTerms => displayedText.Yield(); + } +} diff --git a/osu.Framework.Font/Graphics/Sprites/StyledSpriteText_DrawNode.cs b/osu.Framework.Font/Graphics/Sprites/StyledSpriteText_DrawNode.cs new file mode 100644 index 0000000..80ef5cd --- /dev/null +++ b/osu.Framework.Font/Graphics/Sprites/StyledSpriteText_DrawNode.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Textures; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Graphics.Sprites +{ + public partial class StyledSpriteText + { + internal class SpriteTextDrawNode : TexturedShaderDrawNode + { + protected new StyledSpriteText Source => (StyledSpriteText)base.Source; + + private bool shadow; + private Vector4 shadowOutlineColour; + private Colour4 shadowColour; + private Vector2 shadowOffset; + + private bool outline; + private Colour4 outlineColour; + private float outlineRadius; + + private Vector4 textColour; + + private readonly List parts = new List(); + + public SpriteTextDrawNode(StyledSpriteText source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + parts.Clear(); + parts.AddRange(Source.screenSpaceCharacters); + shadow = Source.Shadow; + outline = Source.Outline; + + var color4 = DrawColourInfo.Colour.AverageColour.Linear; + textColour = new Vector4(color4.R, color4.G, color4.B, color4.A); + + if (shadow) + { + shadowColour = Source.ShadowColour; + shadowOutlineColour = new Vector4(Source.ShadowColour.R, Source.ShadowColour.G, Source.ShadowColour.B, Source.ShadowColour.A); + shadowOffset = Source.premultipliedShadowOffset; + } + + if (outline) + { + outlineColour = Source.OutlineColour; + outlineRadius = Source.outlineRadius / 512; + } + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + var avgColour = (Color4)DrawColourInfo.Colour.AverageColour; + float shadowAlpha = MathF.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); + + //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text. + //squared result for quadratic fall-off seems to give the best result. + //var finalShadowColour = DrawColourInfo.Colour; + //finalShadowColour.ApplyChild(shadowColour.MultiplyAlpha(shadowAlpha)); + + Shader.Bind(); + Shader.GetUniform(@"g_outlineRadius").UpdateValue(ref outlineRadius); + + foreach (var current in parts) + { + if (shadow) + { + Shader.GetUniform(@"g_outlineColour").UpdateValue(ref shadowOutlineColour); + var shadowQuad = current.DrawQuad; + + DrawQuad(current.Texture, + new Quad( + shadowQuad.TopLeft + shadowOffset, + shadowQuad.TopRight + shadowOffset, + shadowQuad.BottomLeft + shadowOffset, + shadowQuad.BottomRight + shadowOffset), + shadowColour, vertexAction: vertexAction, inflationPercentage: current.InflationPercentage); + } + + Shader.GetUniform(@"g_outlineColour").UpdateValue(ref textColour); + + DrawQuad(current.Texture, current.DrawQuad, outlineColour, vertexAction: vertexAction, inflationPercentage: current.InflationPercentage); + } + + Shader.Unbind(); + } + } + + /// + /// A character of a provided with screen space draw coordinates. + /// + internal struct ScreenSpaceCharacterPart + { + /// + /// The screen-space quad for the character to be drawn in. + /// + public Quad DrawQuad; + + /// + /// Extra padding for the character's texture. + /// + public Vector2 InflationPercentage; + + /// + /// The texture to draw the character with. + /// + public Texture Texture; + } + } +}