diff --git a/JollyBit.Canvas.OpenGL.Demo/Program.cs b/JollyBit.Canvas.OpenGL.Demo/Program.cs index f7d98c9..aaa261c 100644 --- a/JollyBit.Canvas.OpenGL.Demo/Program.cs +++ b/JollyBit.Canvas.OpenGL.Demo/Program.cs @@ -117,6 +117,9 @@ protected override void OnRenderFrame(FrameEventArgs e) //Test 2 canvas.MoveTo(200, 200); canvas.LineTo(250, 250); + canvas.QuadraticCurveTo(300, 50, 100, 200); + + canvas.MoveTo(250, 250); canvas.LineTo(100, 200); canvas.Stroke(); canvas.EndBatch(); diff --git a/JollyBit.Canvas/Canvas.cs b/JollyBit.Canvas/Canvas.cs index 5dffad4..fa575ca 100644 --- a/JollyBit.Canvas/Canvas.cs +++ b/JollyBit.Canvas/Canvas.cs @@ -14,6 +14,7 @@ public Canvas() { //Misc InBatch = false; + CurveSmoothness = 0.5f; //Lines LineWidth = 1.0f; LineCap = LineCapStyle.Butt; @@ -120,6 +121,12 @@ public void LineTo(float x, float y) if (subpath == null) return; subpath.LineTo(new Vector2(x, y).ApplyTransform(ref _tranMatrix)); } + public void QuadraticCurveTo(float cpx, float cpy, float x, float y) + { + ComplexSubpath subpath = ensureComplexSubpath(x, y); + if (subpath == null) return; + subpath.QuadraticCurveTo(new Vector2(cpx, cpy).ApplyTransform(ref _tranMatrix), new Vector2(x, y).ApplyTransform(ref _tranMatrix), CurveSmoothness); + } protected ISubpath lastSubpath { get @@ -138,6 +145,11 @@ public void Rect(float x, float y, float w, float h) { _subPaths.Add(new RectSubpath(x, y, w, h)); } + + /// + /// Sets how accurately curves should be drawn. The default value is 0.5 which is smoother than the display device can display. + /// + public float CurveSmoothness { get; set; } #endregion #region Stroking @@ -164,13 +176,6 @@ protected virtual void strokeLineSegment(LineSegment segment1, LineSegment segme public virtual void Stroke() { - Func isPointOnLeft = - (lineStart, lineEnd, point) => - { - //(Bx - Ax) * (Cy - Ay) - (By - Ay) * (Cx - Ax) - return (lineEnd.X - lineStart.X) * (point.Y - lineStart.Y) - (lineEnd.Y - lineStart.Y) * (point.X - lineStart.X) >= 0; - }; - _subPaths.Apply( subpath => { @@ -186,7 +191,7 @@ public virtual void Stroke() { bool pointOnLeft = true; if (segmentPair.Item1 != null && segmentPair.Item2 != null) - pointOnLeft = isPointOnLeft(segmentPair.Item1.StartPoint, segmentPair.Item1.EndPoint, segmentPair.Item2.EndPoint); + pointOnLeft = BezierCurvesHelper.CalcWhatSideOfLinePointIsOn(segmentPair.Item1.StartPoint, segmentPair.Item1.EndPoint, segmentPair.Item2.EndPoint) > 0; strokeLineSegment(segmentPair.Item1, segmentPair.Item2, LineJoinStyle.Miter,pointOnLeft); }); }); @@ -227,7 +232,7 @@ public enum LineJoinStyle { Bevel, Round, - Miter + Miter, } public enum CompositeOperations diff --git a/JollyBit.Canvas/JollyBit.Canvas.csproj b/JollyBit.Canvas/JollyBit.Canvas.csproj index b10f8db..046cf4e 100644 --- a/JollyBit.Canvas/JollyBit.Canvas.csproj +++ b/JollyBit.Canvas/JollyBit.Canvas.csproj @@ -45,6 +45,7 @@ + diff --git a/JollyBit.Canvas/Shapes/BezierCurves.cs b/JollyBit.Canvas/Shapes/BezierCurves.cs new file mode 100644 index 0000000..4178b4a --- /dev/null +++ b/JollyBit.Canvas/Shapes/BezierCurves.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OpenTK; + +namespace JollyBit.Canvas.Shapes +{ + /// + /// A static class with methods for createing bezier curves. Consult http://www.caffeineowl.com/graphics/2d/vectorial/bezierintro.html for a good bezier curves reference. + /// + public static class BezierCurvesHelper + { + #region Utility Functions + /// + /// Calculates the minimum distance between a line and a point. + /// + /// The point at which the line starts + /// The point at which the line ends + /// The point + /// A negative number if point is on right side of line else returns a positive number. Return zero if the point is on the line. + /// The same value as CalcWhatSideOfLinePointIsOn would return. + /// + public static float CalcSquaredDistanceBetweenLineAndPoint(ref Vector2 lineStart, ref Vector2 lineEnd, ref Vector2 point, out float whatSideOfLine) + { + //http://softsurfer.com/Archive/algorithm_0102/algorithm_0102.htm + whatSideOfLine = CalcWhatSideOfLinePointIsOn(lineStart, lineEnd, point); + Vector2 lineDiff; + Vector2.Subtract(ref lineEnd, ref lineStart, out lineDiff); + return (whatSideOfLine * whatSideOfLine) / lineDiff.LengthSquared; + } + + /// + /// Calculates the minimum distance between a line and a point. + /// + /// The point at which the line starts + /// The point at which the line ends + /// The point + public static float CalcSquaredDistanceBetweenLineAndPoint(ref Vector2 lineStart, ref Vector2 lineEnd, ref Vector2 point) + { + float whatSideOfLine; + return CalcSquaredDistanceBetweenLineAndPoint(ref lineStart, ref lineEnd, ref point, out whatSideOfLine); + } + + /// + /// Returns a negative number if point is on right side of line else returns a positive number. Return zero if the point is on the line. + /// + public static float CalcWhatSideOfLinePointIsOn(Vector2 lineStart, Vector2 lineEnd, Vector2 point) + { + return (lineEnd.X - lineStart.X) * (point.Y - lineStart.Y) - (lineEnd.Y - lineStart.Y) * (point.X - lineStart.X); + } + + #endregion + + #region QuadraticBezierCurve + public static IEnumerable CreateQuadraticBezierCurve(Vector2 anchorPoint1, Vector2 anchorPoint2, Vector2 controlPoint, float flatness) + { + flatness = flatness * flatness; + yield return anchorPoint1; + foreach (var item in _createQuadraticBezierCurve(anchorPoint1, anchorPoint2, controlPoint, flatness)) yield return item; + yield return anchorPoint2; + } + private static IEnumerable _createQuadraticBezierCurve(Vector2 anchorPoint1, Vector2 anchorPoint2, Vector2 controlPoint, float flatnessSquared) + { + //1) Check if flat enough + if (CalcSquaredDistanceBetweenLineAndPoint(ref anchorPoint1, ref anchorPoint2, ref controlPoint) > flatnessSquared) + { + //2) Compute nessary values + //segControl1 = (a1+c)/2 + Vector2 segControl1; + Vector2.Add(ref anchorPoint1, ref controlPoint, out segControl1); + Vector2.Divide(ref segControl1, 2f, out segControl1); + //segControl2 = (a1+c)/2 + Vector2 segControl2; + Vector2.Add(ref anchorPoint2, ref controlPoint, out segControl2); + Vector2.Divide(ref segControl2, 2f, out segControl2); + //segAnchor = (segControl1 + segControl2)/2 = (2c+a1+a2)/4 + Vector2 segAnchor; + Vector2.Add(ref segControl1, ref segControl2, out segAnchor); + Vector2.Divide(ref segAnchor, 2f, out segAnchor); + + //3) Recurse segments + foreach (var item in _createQuadraticBezierCurve(anchorPoint1, segAnchor, segControl1, flatnessSquared)) yield return item; + yield return segAnchor; + foreach (var item in _createQuadraticBezierCurve(segAnchor, anchorPoint2, segControl2, flatnessSquared)) yield return item; + } + } + #endregion + + #region CubicBezierCurve + //public static IEnumerable CreateCubicBezierCurve(Vector2 anchorPoint1, Vector2 anchorPoint2, Vector2 controlPoint1, Vector2 controlPoint2, float flatness) + //{ + // flatness = flatness * flatness; + // yield return anchorPoint1; + // foreach (var item in _createCubicBezierCurve(anchorPoint1, anchorPoint2, controlPoint1, controlPoint2, flatness)) yield return item; + // yield return anchorPoint2; + //} + //private static IEnumerable _createCubicBezierCurve(Vector2 anchorPoint1, Vector2 anchorPoint2, Vector2 controlPoint1, Vector2 controlPoint2, float flatnessSquared) + //{ + // //1) Check if flat enough + // //if (CalcSquaredDistanceBetweenLineAndPoint(ref anchorPoint1, ref anchorPoint2, ref controlPoint) > flatnessSquared) + // //{ + // //2) Compute nessary values + // //segControl1 = (a1+c)/2 + // Vector2 segControl1; + // Vector2.Add(ref anchorPoint1, ref controlPoint, out segControl1); + // Vector2.Divide(ref segControl1, 2f, out segControl1); + // //segControl2 = (a1+c)/2 + // Vector2 segControl2; + // Vector2.Add(ref anchorPoint2, ref controlPoint, out segControl2); + // Vector2.Divide(ref segControl2, 2f, out segControl2); + // //segAnchor = (a1 + 3•c1 + 3•c2 + a2)/8 + // Vector2 segAnchor; + // Vector2.Multiply(ref controlPoint, 2f, out segAnchor); + // Vector2.Add(ref segAnchor, ref anchorPoint1, out segAnchor); + // Vector2.Add(ref segAnchor, ref anchorPoint2, out segAnchor); + // Vector2.Divide(ref segAnchor, 4f, out segAnchor); + + // //3) Recurse segments + // foreach (var item in _createQuadraticBezierCurve(anchorPoint1, segAnchor, segControl1, flatnessSquared)) yield return item; + // yield return segAnchor; + // foreach (var item in _createQuadraticBezierCurve(segAnchor, anchorPoint2, segControl2, flatnessSquared)) yield return item; + // //} + //} + #endregion + } +} diff --git a/JollyBit.Canvas/Shapes/ComplexSubpath.cs b/JollyBit.Canvas/Shapes/ComplexSubpath.cs index 1d2c307..2f5f670 100644 --- a/JollyBit.Canvas/Shapes/ComplexSubpath.cs +++ b/JollyBit.Canvas/Shapes/ComplexSubpath.cs @@ -8,7 +8,7 @@ namespace JollyBit.Canvas.Shapes { public class ComplexSubpath : ISubpath { - private IList _vectors = new List(); + private List _vectors = new List(); public ComplexSubpath(Vector2 startPoint) { _vectors.Add(startPoint); @@ -24,6 +24,11 @@ public void LineTo(Vector2 point) _vectors.Add(point); } + public void QuadraticCurveTo(Vector2 controlPoint, Vector2 anchorPoint2, float flatness) + { + _vectors.AddRange(BezierCurvesHelper.CreateQuadraticBezierCurve(lastVector, anchorPoint2, controlPoint, flatness)); + } + public void ClosePath() { if (!IsClosed) LineTo(_vectors[0]); @@ -32,7 +37,12 @@ public void ClosePath() public bool IsClosed { - get { return _vectors.Count > 1 && _vectors[0] == _vectors[_vectors.Count - 1]; } + get { return _vectors.Count > 1 && _vectors[0] == lastVector; } + } + + private Vector2 lastVector + { + get { return _vectors[_vectors.Count - 1]; } } public IEnumerator GetEnumerator()