diff --git a/Quik/QuikContext.cs b/Quik/QuikContext.cs index 5fb4117..6687880 100644 --- a/Quik/QuikContext.cs +++ b/Quik/QuikContext.cs @@ -1,4 +1,6 @@ -namespace Quik +using Quik.Typography; + +namespace Quik { /// /// An object which QUIK commands may be issued to. @@ -10,11 +12,13 @@ /// public QuikDraw Draw { get; } = new QuikDraw(); - public QuikStrokeStyle DefaultStroke { get; set; } = new QuikStrokeStyle(new QuikColor(0x000000FF), 4); + public QuikStrokeStyle DefaultStroke { get; set; } = new QuikStrokeStyle(new QuikColor(0xaaaaaaff), 4); public QuikFillStyle DefaultFill { get; set; } = new QuikFillStyle() { - Color = new QuikColor(0xA0A0A0FF) + Color = new QuikColor(0xeeeeeeff) }; + + public QuikFont DefaultFont { get; set; } } } \ No newline at end of file diff --git a/Quik/QuikDraw.cs b/Quik/QuikDraw.cs index eafb467..7d978af 100644 --- a/Quik/QuikDraw.cs +++ b/Quik/QuikDraw.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO.Compression; using System.Linq; namespace Quik @@ -33,5 +34,8 @@ namespace Quik public void StencilClear() => Commands.Enqueue(new QuikCommandStencilClear()); public void StencilBegin() => Commands.Enqueue(new QuikCommandStencilBegin()); public void StencilEnd() => Commands.Enqueue(new QuikCommandStencilEnd()); + public void PutChar(char chr, QuikVec2 position) => Commands.Enqueue(new QuikCommandPutChar(chr, position)); + public void PutText(string text, QuikVec2 position) => Commands.Enqueue(new QuikCommandPutText(text, position)); + public void FlowText(string text, QuikRectangle bounds) => Commands.Enqueue(new QuikCommandFlowText(text, bounds)); } } \ No newline at end of file diff --git a/Quik/Typography/QuikFont.cs b/Quik/Typography/QuikFont.cs new file mode 100644 index 0000000..e13b751 --- /dev/null +++ b/Quik/Typography/QuikFont.cs @@ -0,0 +1,14 @@ +namespace Quik.Typography +{ + public abstract class QuikFont + { + public abstract QuikFontStyle Style { get; } + + public string FontFamily => Style.Family; + public float FontSize => Style.Size; + public QuikFontStyle FontStyle => Style; + + public abstract bool HasCharacter(int character); + public abstract void GetCharacter(int character, out int texture, out QuikGlyph glyph); + } +} \ No newline at end of file diff --git a/Quik/Typography/QuikFontStyle.cs b/Quik/Typography/QuikFontStyle.cs new file mode 100644 index 0000000..78c4468 --- /dev/null +++ b/Quik/Typography/QuikFontStyle.cs @@ -0,0 +1,23 @@ +namespace Quik.Typography +{ + public class QuikFontStyle + { + public string Family { get; } + public QuikFontType Type { get; } + public float Size { get; } + + public QuikFontStyle(string family, float size, QuikFontType type = QuikFontType.Normal) + { + Family = family; + Size = size; + Type = type; + } + } + + public enum QuikFontType + { + Normal, + Italic, + Bold + } +} \ No newline at end of file diff --git a/Quik/Typography/QuikGlyph.cs b/Quik/Typography/QuikGlyph.cs new file mode 100644 index 0000000..437d22c --- /dev/null +++ b/Quik/Typography/QuikGlyph.cs @@ -0,0 +1,54 @@ +namespace Quik.Typography +{ + /// + /// Glyph properties with metrics based on FreeType glyph metrics. + /// + public struct QuikGlyph + { + /// + /// The code point for the character. + /// + public int Character { get; } + + /// + /// Location of the glyph on the atlas. + /// + public QuikRectangle Location { get; } + + /// + /// Size of the glyph in units. + /// + public QuikVec2 Size { get; } + + /// + /// Bearing vector for horizontal layout. + /// + public QuikVec2 HorizontalBearing { get; } + + /// + /// Bearing vector for vertical layout. + /// + public QuikVec2 VerticalBearing { get; } + + /// + /// Advance vector for vertical and horizontal layouts. + /// + public QuikVec2 Advance { get; } + + public QuikGlyph( + int character, + QuikRectangle location, + QuikVec2 size, + QuikVec2 horizontalBearing, + QuikVec2 verticalBearing, + QuikVec2 advance) + { + Character = character; + Location = location; + Size = size; + HorizontalBearing = horizontalBearing; + VerticalBearing = verticalBearing; + Advance = advance; + } + } +} \ No newline at end of file diff --git a/Quik/VertexGenerator/QuikVertexGenerator.cs b/Quik/VertexGenerator/QuikVertexGenerator.cs index 8e054c9..8cd88a9 100644 --- a/Quik/VertexGenerator/QuikVertexGenerator.cs +++ b/Quik/VertexGenerator/QuikVertexGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Quik.Typography; namespace Quik.VertexGenerator { @@ -223,9 +224,17 @@ namespace Quik.VertexGenerator case QuikCommandType.Rectangles: RenderRectangles(command as QuikCommandRectangles); goto exit_with_call; + + case QuikCommandType.PutChar: + RenderCharacter(command as QuikCommandPutChar); + goto exit_with_call; + + case QuikCommandType.PutText: + RenderTextPut(command as QuikCommandPutText); + goto exit_with_call; + default: { - QuikDrawCall? subcall = null; if (OnHandleCommand(this, command)) goto exit_with_call; break; @@ -1393,6 +1402,97 @@ namespace Quik.VertexGenerator } #endregion + + #region Text + + private void RenderCharacter(QuikCommandPutChar chr) + { + Context.DefaultFont.GetCharacter(chr.Character, out int texture, out QuikGlyph metrics); + + QuikVertex a, b, c, d; + a = b = c = d = new QuikVertex() {Color = new QuikColor(0xffffffff)}; + + a.Position = chr.Position + new QuikVec2(0, metrics.HorizontalBearing.Y - metrics.Size.Y); + a.TextureCoordinates = metrics.Location.Min; + + b.Position = a.Position + new QuikVec2(metrics.Size.X, 0); + c.Position = a.Position + metrics.Size; + d.Position = a.Position + new QuikVec2(0, metrics.Size.Y); + + b.TextureCoordinates = new QuikVec2(metrics.Location.Right, metrics.Location.Bottom); + c.TextureCoordinates = metrics.Location.Max; + d.TextureCoordinates = new QuikVec2(metrics.Location.Left, metrics.Location.Top); + + short startVertex = (short)_vertexBufferPointer; + short startElement = (short) _elementBufferPointer; + AddVertex(a, b, c, d); + AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); + + QuikDrawCall call = CallTemplate; + call.Texture = texture; + call.Offset = (short) (startElement * 2); + call.Count = 6; + DrawCalls.Add(call); + } + + private void RenderTextPut(QuikCommandPutText text) + { + short startElement = (short)_elementBufferPointer; + QuikFont font = Context.DefaultFont; + QuikVertex vertex = new QuikVertex() {Color = new QuikColor(0xff7777ff)}; + QuikVec2 pointer = text.Position; + int texture = -1; + + for (int i = 0; i < text.Text.Length; i++) + { + int chr = text.Text[i]; + QuikGlyph metrics; + + int ntex; + font.GetCharacter(chr, out ntex, out metrics); + + if (ntex != texture && texture != -1) + { + QuikDrawCall call = CallTemplate; + call.Texture = texture; + call.Offset = (short) (startElement * 2); + call.Count = (short)(_elementBufferPointer - startElement); + DrawCalls.Add(call); + + startElement = (short) _elementBufferPointer; + } + + texture = ntex; + QuikVertex a, b, c, d; + a = b = c = d = vertex; + + a.Position = pointer + new QuikVec2(0, metrics.HorizontalBearing.Y - metrics.Size.Y); + a.TextureCoordinates = metrics.Location.Min; + + b.Position = a.Position + new QuikVec2(metrics.Size.X, 0); + c.Position = a.Position + metrics.Size; + d.Position = a.Position + new QuikVec2(0, metrics.Size.Y); + + b.TextureCoordinates = new QuikVec2(metrics.Location.Right, metrics.Location.Bottom); + c.TextureCoordinates = metrics.Location.Max; + d.TextureCoordinates = new QuikVec2(metrics.Location.Left, metrics.Location.Top); + + pointer.X += metrics.Advance.X; + + short startVertex = (short)_vertexBufferPointer; + AddVertex(a, b, c, d); + AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); + } + + { + QuikDrawCall call = CallTemplate; + call.Texture = texture; + call.Offset = (short) (startElement * 2); + call.Count = (short)(_elementBufferPointer - startElement); + DrawCalls.Add(call); + } + } + #endregion } public delegate void VertexGeneratorCommandHandler(QuikVertexGenerator generator, QuikCommand command, ref bool anyCalls); @@ -1410,5 +1510,6 @@ namespace Quik.VertexGenerator public short Count; public QuikRectangle Bounds; public bool ClearStencil; + public int Texture; } } \ No newline at end of file diff --git a/QuikTestApplication/Program.cs b/QuikTestApplication/Program.cs index de276ca..44f1148 100644 --- a/QuikTestApplication/Program.cs +++ b/QuikTestApplication/Program.cs @@ -35,9 +35,11 @@ in vec2 ftexcoord; in vec4 fcolor; out vec4 outcolor; +uniform sampler2D texture0; + void main() { - outcolor = fcolor; + outcolor = fcolor * texture(texture0, ftexcoord); } "; @@ -96,7 +98,7 @@ void main() QuikVertex.PositionOffset); GL.EnableVertexAttribArray(loc); GL.VertexAttribPointer( - loc = GL.GetAttribLocation(sp, "texcoords"), + loc = GL.GetAttribLocation(sp, "texcoord"), 2, VertexAttribPointerType.Float, false, @@ -133,6 +135,31 @@ void main() Color = new QuikColor(0xeeeeeeff) }; + + int whiteTexture = GL.GenTexture(); + uint[] whitePixel = {0xFFFFFFFF}; + GL.BindTexture(TextureTarget.Texture2D, whiteTexture); + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, 1, 1, 0, PixelFormat.Rgba, PixelType.UnsignedByte, whitePixel); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)All.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)All.Nearest); + + TestFont font = new TestFont(); + font.TextureBase = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, font.TextureBase); + GL.TexImage2D( + TextureTarget.Texture2D, + 0, + PixelInternalFormat.Rgb, + (int)TestFont.TextureSize.X, (int)TestFont.TextureSize.Y, 0, + PixelFormat.Rgba, PixelType.UnsignedByte, + TestFont.Texture); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)All.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)All.Nearest); + + context.DefaultFont = font; + + window.Context.SwapInterval = 0; + for (;!window.IsExiting;) { NativeWindow.ProcessWindowEvents(false); @@ -144,9 +171,9 @@ void main() Matrix4 matrix = Matrix4.CreateOrthographicOffCenter( 0, - (float)window.Size.X / 2, + (float)window.Size.X, 0, - (float)window.Size.Y / 2, + (float)window.Size.Y, 1, -1); GL.UniformMatrix4(loc, false, ref matrix); @@ -198,6 +225,8 @@ void main() FillStyle = fill, StrokeStyle = strokeBorder }); + + context.Draw.PutText("Aaah! the cat turned into a cat!", new QuikVec2(25,30)); QuikCommand command; while (context.Draw.Commands.TryDequeue(out command)) @@ -210,6 +239,7 @@ void main() foreach (QuikDrawCall call in gen.DrawCalls) { + GL.BindTexture(TextureTarget.Texture2D, call.Texture == 0 ? whiteTexture : call.Texture); GL.DrawElements(BeginMode.Triangles, call.Count, DrawElementsType.UnsignedShort, call.Offset); } diff --git a/QuikTestApplication/QuikTestApplication.csproj b/QuikTestApplication/QuikTestApplication.csproj index 786da43..b4f36c4 100644 --- a/QuikTestApplication/QuikTestApplication.csproj +++ b/QuikTestApplication/QuikTestApplication.csproj @@ -9,4 +9,11 @@ + + + + + + + diff --git a/QuikTestApplication/TestFont.cs b/QuikTestApplication/TestFont.cs new file mode 100644 index 0000000..66977ee --- /dev/null +++ b/QuikTestApplication/TestFont.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Quik; +using Quik.Typography; + +namespace QuikTestApplication +{ + public class TestFont : QuikFont + { + public override QuikFontStyle Style => _style; + + private static QuikFontStyle _style; + private static HashSet _characters = new HashSet(); + private static Dictionary _glyphs = new Dictionary(); + + public static byte[] Texture { get; private set; } = Array.Empty(); + public static QuikVec2 TextureSize { get; private set; } + + public int TextureBase { get; set; } = 1; + + static TestFont() + { + using (Stream str = typeof(TestFont).Assembly.GetManifestResourceStream("QuikTestApplication.font.xml")) + { + XmlDocument document = new XmlDocument(); + document.Load(str); + + string family = document.DocumentElement.Attributes["name"].Value; + float fontSize = float.Parse(document.DocumentElement.Attributes["size"].Value); + QuikFontType style = QuikFontType.Normal; + if (document.DocumentElement.Attributes["bold"].Value == "true") + style = QuikFontType.Bold; + if (document.DocumentElement.Attributes["italic"].Value == "true") + style = QuikFontType.Italic; + + _style = new QuikFontStyle(family, fontSize, style); + + QuikVec2 atlasSize = default; + atlasSize.X = float.Parse(document.DocumentElement.Attributes["width"].Value); + atlasSize.Y = float.Parse(document.DocumentElement.Attributes["height"].Value); + + TextureSize = atlasSize; + + foreach (XmlElement element in document.SelectNodes("/font/character")) + { + QuikRectangle UVs; + QuikVec2 origin; + float advance; + int chr = element.Attributes["text"].Value[0]; + + QuikVec2 pos; + QuikVec2 size; + pos.X = float.Parse(element.Attributes["x"].Value); + pos.Y = float.Parse(element.Attributes["y"].Value); + size.X = float.Parse(element.Attributes["width"].Value); + size.Y = float.Parse(element.Attributes["height"].Value); + UVs = new QuikRectangle( + (pos.X + size.X)/atlasSize.X, + pos.Y/atlasSize.Y, + pos.X/atlasSize.X, + (size.Y + pos.Y)/atlasSize.Y); + + origin.X = float.Parse(element.Attributes["origin-x"].Value); + origin.Y = float.Parse(element.Attributes["origin-y"].Value); + + advance = float.Parse(element.Attributes["advance"].Value); + + QuikGlyph glyph = new QuikGlyph(chr, UVs, size, origin, default, new QuikVec2(advance, 0)); + _glyphs.Add(chr, glyph); + _characters.Add(chr); + } + } + + using (Stream str = typeof(TestFont).Assembly.GetManifestResourceStream("QuikTestApplication.font.dat")) + { + Texture = new byte[(int)str.Length]; + str.Read(Texture, 0, (int)str.Length); + } + } + + public override bool HasCharacter(int character) + { + return _characters.Contains((char) character); + } + + public override void GetCharacter(int character, out int texture, out QuikGlyph glyph) + { + if (!_glyphs.TryGetValue(character, out glyph)) + { + glyph = _glyphs['?']; + } + + texture = TextureBase; + } + } +} \ No newline at end of file diff --git a/QuikTestApplication/font.dat b/QuikTestApplication/font.dat new file mode 100644 index 0000000..1c997c7 Binary files /dev/null and b/QuikTestApplication/font.dat differ diff --git a/QuikTestApplication/font.png b/QuikTestApplication/font.png new file mode 100644 index 0000000..eaa57da Binary files /dev/null and b/QuikTestApplication/font.png differ diff --git a/QuikTestApplication/font.xml b/QuikTestApplication/font.xml new file mode 100644 index 0000000..7ace8aa --- /dev/null +++ b/QuikTestApplication/font.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file