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