diff --git a/Quik/CommandMachine/CommandEngine.cs b/Quik/CommandMachine/CommandEngine.cs index 0c3ef89..fdb8771 100644 --- a/Quik/CommandMachine/CommandEngine.cs +++ b/Quik/CommandMachine/CommandEngine.cs @@ -21,7 +21,7 @@ namespace Quik.CommandMachine // TODO: Make a real matrix class. public float[] ActiveTransforms { get; } - public object ActiveStyle { get; } + public StyleStack Style { get; } protected CommandEngine() { diff --git a/Quik/IQuikTexture.cs b/Quik/IQuikTexture.cs index 5533565..83f68b4 100644 --- a/Quik/IQuikTexture.cs +++ b/Quik/IQuikTexture.cs @@ -25,9 +25,13 @@ namespace Quik /// /// Indicates whether this texture contains a signed distance field. /// - /// public bool SignedDistanceField { get; set; } + /// + /// Indicates whether this texture has premultiplied alpha. + /// + public bool PreMultipled { get; set; } + /// /// Upload texture data. /// diff --git a/Quik/QuikStyle.cs b/Quik/QuikStyle.cs index 5b1e436..db3cfe5 100644 --- a/Quik/QuikStyle.cs +++ b/Quik/QuikStyle.cs @@ -114,16 +114,16 @@ namespace Quik set => this["list-marker-image"] = value; } - public float? BorderWidth + public float? StrokeWidth { - get => (float?)this["border-width"]; - set => this["border-width"] = value; + get => (float?)this["stroke-width"]; + set => this["stroke-width"] = value; } - public QuikColor? BorderColor + public QuikColor? StrokeColor { - get => (QuikColor?)this["border-color"]; - set => this["border-color"] = value; + get => (QuikColor?)this["stroke-color"]; + set => this["stroke-color"] = value; } public QuikFont Font @@ -131,6 +131,12 @@ namespace Quik get => (QuikFont)this["font"]; set => this["font"] = value; } + + public int? ZIndex + { + get => (int?)this["z-index"]; + set => this["z-index"] = value; + } } public class Style : StyleBase diff --git a/Quik/VertexGenerator/DrawQueue.cs b/Quik/VertexGenerator/DrawQueue.cs new file mode 100644 index 0000000..a905bd1 --- /dev/null +++ b/Quik/VertexGenerator/DrawQueue.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Quik.VertexGenerator +{ + public class DrawQueue + { + private readonly RefList _vertices = new RefList(); + private readonly RefList _elements = new RefList(); + private readonly List _drawCalls = new List(); + private int _start; + private int _count; + private int _baseOffset; + private QuikRectangle _bounds; + private QuikTexture _texture; + + public int ZDepth { get; private set; } + public QuikVertex[] VertexArray => _vertices.InternalArray; + public int VertexCount => _vertices.Count; + public int[] ElementArray => _elements.InternalArray; + public int ElementCount => _elements.Count; + public int DrawCallCount => _drawCalls.Count; + public int BaseOffset => _baseOffset; + + public void Clear() + { + _vertices.Clear(); + _elements.Clear(); + _drawCalls.Clear(); + ZDepth = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void StartDrawCall(in QuikRectangle bounds, QuikTexture texture, int baseOffset) + { + _start = ElementCount; + _count = 0; + _texture = texture; + _bounds = bounds; + _baseOffset = baseOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void StartDrawCall(in QuikRectangle bounds) => StartDrawCall(bounds, null, _vertices.Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void StartDrawCall(in QuikRectangle bounds, int baseOffset) => StartDrawCall(bounds, null, baseOffset); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void StartDrawCall(in QuikRectangle bounds, QuikTexture texture) => StartDrawCall(bounds, texture, _vertices.Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddVertex(in QuikVertex vertex) + { + _vertices.Add(in vertex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddElement(int offset) + { + _elements.Add(offset + _baseOffset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int RestoreOffset(int baseOffset) + { + int old = _baseOffset; + _baseOffset = baseOffset; + return old; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int RestoreOffset() => RestoreOffset(_vertices.Count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int AbsoluteElement(int offset) + { + return _baseOffset + offset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int RelativeElement(int baseOffset, int offset) + { + return AbsoluteElement(offset) - baseOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EndDrawCall() + { + _drawCalls.Add(new DrawCall(_start, _count, _bounds, _texture)); + } + } + + public struct DrawCall + { + public int Start { get; } + public int Count { get; } + public QuikRectangle Bounds { get; } + public QuikTexture Texture { get; } + + public DrawCall(int start, int count, in QuikRectangle bounds, QuikTexture texture) + { + Start = start; + Count = count; + Bounds = bounds; + Texture = texture; + } + } +} \ No newline at end of file diff --git a/Quik/VertexGenerator/QuikVertex.cs b/Quik/VertexGenerator/QuikVertex.cs index 7688526..e34db6d 100644 --- a/Quik/VertexGenerator/QuikVertex.cs +++ b/Quik/VertexGenerator/QuikVertex.cs @@ -17,15 +17,21 @@ namespace Quik.VertexGenerator /// Texture Coordinates. /// public QuikVec2 TextureCoordinates; - + /// /// Per vertex color value. /// public QuikColor Color; + /// + /// Per vertex depth index value. + /// + public int ZIndex; + public static int PositionOffset => 0; public static unsafe int TextureCoordinatesOffset => sizeof(QuikVec2); public static unsafe int ColorOffset => 2 * sizeof(QuikVec2); + public static unsafe int ZIndexOffset => ColorOffset + sizeof(QuikColor); public static unsafe int Stride => sizeof(QuikVertex); } } \ No newline at end of file diff --git a/Quik/VertexGenerator/RefList.cs b/Quik/VertexGenerator/RefList.cs new file mode 100644 index 0000000..10f20a3 --- /dev/null +++ b/Quik/VertexGenerator/RefList.cs @@ -0,0 +1,48 @@ +using System; + +namespace Quik.VertexGenerator +{ + /// + /// A small list which whose items can be used by reference. + /// + /// Container type. + public class RefList + { + private T[] _array = Array.Empty(); + private int _count = 0; + + public T[] InternalArray => _array; + + public ref T this[int index] => ref _array[index]; + + public int Count => _count; + + public int Capacity => _array.Length; + + public void Add(in T item) + { + EnsureCapacity(Count + 1); + this[_count++] = item; + } + + public void Add(T item) + { + EnsureCapacity(Count + 1); + this[_count++] = item; + } + + public void Clear() + { + Array.Resize(ref _array, 0); + _count = 0; + } + + private void EnsureCapacity(int needed) + { + while (_array.Length < needed) + { + Array.Resize(ref _array, Math.Max(1, _array.Length) * 2); + } + } + } +} \ No newline at end of file diff --git a/Quik/VertexGenerator/VertexCommandEngine.cs b/Quik/VertexGenerator/VertexCommandEngine.cs new file mode 100644 index 0000000..2d827ae --- /dev/null +++ b/Quik/VertexGenerator/VertexCommandEngine.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using Quik.CommandMachine; + +namespace Quik.VertexGenerator +{ + public class VertexGeneratorEngine : CommandEngine + { + public DrawQueue DrawQueue { get; } = new DrawQueue(); + + /// + /// Granularity for rounded geometry. + /// + protected float CurveGranularity => + (Style["-vertex-curve-granularity"] is float value) ? value : 1.0f; + protected QuikVertex StrokeVertex => new QuikVertex() + { + ZIndex = Style.ZIndex ?? this.ZIndex, + Color = Style.StrokeColor ?? QuikColor.Black, + }; + protected QuikVertex FillVertex => new QuikVertex() + { + ZIndex = Style.ZIndex ?? this.ZIndex, + Color = Style.Color ?? QuikColor.White, + }; + + protected override void ChildProcessCommand(Command name, CommandQueue queue) + { + base.ChildProcessCommand(name, queue); + + switch(name) + { + case Command.Line: LineProc(queue); break; + default: + break; + } + } + + /// + /// Gets the rounding resolution for a line segment or border radius. + /// + /// The width of the line. + /// The angle of the cap or joint arc. + /// The rounding resolution. + protected int GetRoundingResolution(float radius, float arc) + { + return (int) Math.Ceiling(arc * radius * CurveGranularity); + } + + private readonly List LineList = new List(); + private void LineProc(CommandQueue queue) + { + Frame frame = queue.Dequeue(); + + // Clear temporary vector list and retreive all line segments. + LineList.Clear(); + if (frame.Type == FrameType.IVec1) + { + int count = (int)frame; + for (int i = 0; i < count; i++) + { + frame = queue.Dequeue(); + LineList.Add((QuikLine)frame); + } + } + else + { + LineList.Add((QuikLine)frame); + } + + float width = Style.StrokeWidth ?? 1; + + DrawQueue.StartDrawCall(Viewport); + LineInfo prevBase, nextBase = default; + for (int i = 0; i < LineList.Count; i++) + { + QuikLine line = LineList[i]; + // A line segment needs a start cap if it is the first segment in + // the list, or the last end point is not the current start point. + bool isStart = (i == 0 || line.Start != LineList[i - 1].End); + // A line segment needs an end cap if it is the last line segment + // in the list or if the next start point is not the current end point. + bool isEnd = (i == LineList.Count - 1 || line.End != LineList[i+1].Start); + + // Generate the main line segment. + prevBase = nextBase; + nextBase = GenerateLineSegment(line); + + if (isStart) + { + // Then a start cap if necessary. + GenerateCap(line.Start, line.Normal(), prevBase, false); + } + else + { + // Otherwise generate the required joint. + GenerateJoint(line.Start, LineList[i-1].Normal(), line.Normal(), prevBase, nextBase); + } + if (isEnd) + { + // Then generate the end cap if necessary. + GenerateCap(line.End, line.Normal(), nextBase, true); + } + } + DrawQueue.EndDrawCall(); + } + + private LineInfo GenerateLineSegment(in QuikLine line) + { + QuikVertex vertex = StrokeVertex; + QuikVertex a, b, c, d; + QuikVec2 normal = line.Normal(); + float width = Style.StrokeWidth ?? 1; + + a = b = c = d = vertex; + a.Position = line.Start + width / 2 * normal; + b.Position = line.Start - width / 2 * normal; + c.Position = line.End + width / 2 * normal; + d.Position = line.End - width / 2 * normal; + + DrawQueue.RestoreOffset(); + DrawQueue.AddVertex(a); + DrawQueue.AddVertex(b); + DrawQueue.AddVertex(c); + DrawQueue.AddVertex(d); + DrawQueue.AddElement(1); DrawQueue.AddElement(2); DrawQueue.AddElement(0); + DrawQueue.AddElement(1); DrawQueue.AddElement(3); DrawQueue.AddElement(2); + return new LineInfo(DrawQueue.BaseOffset, 0, 1, 2, 3); + } + + private void GenerateJoint( + in QuikVec2 center, + in QuikVec2 prevNormal, + in QuikVec2 nextNormal, + in LineInfo prevInfo, + in LineInfo nextInfo) + { + // Figure out which side needs the joint. + QuikVec2 meanNormal = 0.5f * (prevNormal + nextNormal); + QuikVec2 meanTangent = new QuikVec2(meanNormal.Y, -meanNormal.X); + QuikVec2 positiveEdge = ((center + nextNormal) - (center + prevNormal)).Normalize(); + QuikVec2 negativeEdge = ((center - nextNormal) - (center - prevNormal)).Normalize(); + float positive, negative; + positive = QuikVec2.Dot(meanTangent, positiveEdge); + negative = QuikVec2.Dot(meanNormal, negativeEdge); + + if (positive == negative) + { + // To be fair this is highly unlikely considering the nature of + // floats, but, generate an end cap to handle a cusp. + GenerateCap(center, nextNormal, nextInfo, true); + return; + } + + QuikVertex vertex = StrokeVertex; + float radius = Style.StrokeWidth/2 ?? 0.5f; + float arc = MathF.Acos(QuikVec2.Dot(prevNormal, nextNormal)); + int resolution = GetRoundingResolution(radius, arc); + bool isNegative = positive < negative; + + vertex.Position = center; + DrawQueue.RestoreOffset(); + DrawQueue.AddVertex(vertex); + + int lastIndex, endIndex; + if (isNegative) + { + lastIndex = DrawQueue.RelativeElement(prevInfo.BaseOffset, prevInfo.EndNegative); + endIndex = DrawQueue.RelativeElement(nextInfo.BaseOffset, nextInfo.StartNegative); + } + else + { + lastIndex = DrawQueue.RelativeElement(nextInfo.BaseOffset, nextInfo.StartNegative); + endIndex = DrawQueue.RelativeElement(prevInfo.BaseOffset, prevInfo.EndPositive); + } + + for (int i = 0; i < resolution; i++) + { + float angle = (float)(i+1) / (resolution + 1) * arc; + float cos = MathF.Cos(angle); + float sin = MathF.Sin(angle); + + QuikVec2 displacement; + if (isNegative) + { + displacement = new QuikVec2() + { + X = -prevNormal.X * cos + prevNormal.Y * sin, + Y = -prevNormal.X * sin - prevNormal.Y * cos + } * radius; + } + else + { + displacement = new QuikVec2() + { + X = nextNormal.X * cos - nextNormal.Y * sin, + Y = nextNormal.X * sin + nextNormal.Y * cos + } * radius; + } + + vertex.Position = center + displacement; + + DrawQueue.AddVertex(vertex); + DrawQueue.AddElement(lastIndex); + DrawQueue.AddElement(i); + DrawQueue.AddElement(0); + + lastIndex = i; + } + + DrawQueue.AddElement(lastIndex); + DrawQueue.AddElement(endIndex); + DrawQueue.AddElement(0); + + } + + private void GenerateCap( + in QuikVec2 center, + in QuikVec2 normal, + in LineInfo info, + bool endCap) + { + int lastIndex, startIndex; + QuikVertex vertex = StrokeVertex; + float radius = Style.StrokeWidth ?? 1.0f; + int resolution = GetRoundingResolution(radius, MathF.PI); + + DrawQueue.RestoreOffset(); + if (endCap) + { + lastIndex = DrawQueue.RelativeElement(info.BaseOffset, info.EndPositive); + startIndex = DrawQueue.RelativeElement(info.BaseOffset, info.EndNegative); + } + else + { + lastIndex = DrawQueue.RelativeElement(info.BaseOffset, info.StartPositive); + startIndex = DrawQueue.RelativeElement(info.BaseOffset, info.StartNegative); + } + + for (int i = 0; i < resolution; i++) + { + float angle = (float) (i + 1) / (resolution + 1) * MathF.PI; + float cos = MathF.Cos(angle); + float sin = MathF.Sin(angle); + + QuikVec2 displacement; + if (endCap) + { + displacement = new QuikVec2() + { + X = normal.X * cos + normal.Y * sin, + Y = -normal.X * sin + normal.Y * cos + } * radius; + } + else + { + displacement = new QuikVec2() + { + X = normal.X * cos - normal.Y * sin, + Y = normal.X * sin + normal.Y * cos + } * radius; + } + + vertex.Position = center + displacement; + + DrawQueue.AddVertex(vertex); + DrawQueue.AddElement(startIndex); + DrawQueue.AddElement(lastIndex); + DrawQueue.AddElement(i); + + lastIndex = i; + } + } + + private readonly List BezierList = new List(); + private void BezierProc(CommandQueue queue) + { + Frame a = queue.Dequeue(); + Frame b; + + // Clear temporary vector list and retreive all bezier segments. + BezierList.Clear(); + if (a.Type == FrameType.IVec1) + { + int count = (int)a; + for (int i = 0; i < count; i++) + { + a = queue.Dequeue(); + b = queue.Dequeue(); + + BezierList.Add( + new QuikBezier( + new QuikVec2(a.GetF(0), a.GetF(1)), + new QuikVec2(b.GetF(0), b.GetF(1)), + new QuikVec2(b.GetF(2), b.GetF(3)), + new QuikVec2(a.GetF(2), a.GetF(3)) + ) + ); + } + } + else + { + b = queue.Dequeue(); + + BezierList.Add( + new QuikBezier( + new QuikVec2(a.GetF(0), a.GetF(1)), + new QuikVec2(b.GetF(0), b.GetF(1)), + new QuikVec2(b.GetF(2), b.GetF(3)), + new QuikVec2(a.GetF(2), a.GetF(3)) + ) + ); + } + + float width = Style.StrokeWidth ?? 1; + + DrawQueue.StartDrawCall(Viewport); + LineInfo prevBase, nextBase = default; + for (int i = 0; i < LineList.Count; i++) + { + QuikBezier bezier = BezierList[i]; + // A line segment needs a start cap if it is the first segment in + // the list, or the last end point is not the current start point. + bool isStart = (i == 0 || bezier.Start != BezierList[i - 1].End); + // A line segment needs an end cap if it is the last line segment + // in the list or if the next start point is not the current end point. + bool isEnd = (i == LineList.Count - 1 || bezier.End != BezierList[i+1].Start); + + // Generate the main line segment. + prevBase = nextBase; + nextBase = GenerateBezierSegment(bezier); + + if (isStart) + { + // Then a start cap if necessary. + GenerateCap(bezier.Start, bezier.GetBezierNormal(0), prevBase, false); + } + else + { + // Otherwise generate the required joint. + GenerateJoint(bezier.Start, BezierList[i-1].GetBezierNormal(1), bezier.GetBezierNormal(0), prevBase, nextBase); + } + if (isEnd) + { + // Then generate the end cap if necessary. + GenerateCap(bezier.End, bezier.GetBezierNormal(1), nextBase, true); + } + } + DrawQueue.EndDrawCall(); + } + + private LineInfo GenerateBezierSegment(in QuikBezier bezier) + { + QuikVec2 startTangent = bezier.GetBezierTangent(0); + QuikVec2 endTangent = bezier.GetBezierTangent(1); + QuikVec2 startNormal = new QuikVec2(-startTangent.Y, startTangent.X).Normalize(); + QuikVec2 endNormal = new QuikVec2(-endTangent.Y, endTangent.X).Normalize(); + + float width = Style.StrokeWidth ?? 1; + float radius = 0.5f * width; + int resolution = GetRoundingResolution(radius, bezier.RasterizationArc); + + DrawQueue.RestoreOffset(); + QuikVertex v = StrokeVertex; + int vbase = DrawQueue.BaseOffset; + int index = 2; + + v.Position = bezier.Start + radius * startNormal; + DrawQueue.AddVertex(v); + v.Position = bezier.Start - radius * startNormal; + DrawQueue.AddVertex(v); + + for (int i = 0; i < resolution; i++, index += 2) + { + float t = (i + 1.0f) / resolution; + QuikVec2 at = bezier.GetBezierTangent(t).Normalize(); + QuikVec2 a = bezier.GetBezierPoint(t); + QuikVec2 an = radius * new QuikVec2(-at.Y, at.X); + + v.Position = a + an; + DrawQueue.AddVertex(v); + v.Position = a - an; + DrawQueue.AddVertex(v); + + DrawQueue.AddElement(index - 2); DrawQueue.AddElement(index - 1); DrawQueue.AddElement(index + 0); + DrawQueue.AddElement(index - 1); DrawQueue.AddElement(index + 1); DrawQueue.AddElement(index + 0); + } + + return new LineInfo(vbase, 0, 1, index - 2, index - 1); + } + + private struct LineInfo + { + public int BaseOffset { get; } + public int StartPositive { get; } + public int StartNegative { get; } + public int EndPositive { get; } + public int EndNegative { get; } + + public LineInfo(int baseOffset, int startPositive, int startNegative, int endPositive, int endNegative) + { + BaseOffset = baseOffset; + StartPositive = startPositive; + StartNegative = startNegative; + EndPositive = endPositive; + EndNegative = endNegative; + } + } + } +} \ No newline at end of file