/// <summary> /// Create a sprite that follows a Bezier path. /// /// Path format is (similar to svg) (g=guidepoint): /// g1p1x,g1p1y[space]g2p1x,g2p1y[space]p1x,p1y /// g1p2x,g1p2y[space]g2p2x,g2p2y[space]p2x,p2y /// etc... /// So it's 2 guidepoints followed by end point. /// This will follow simple Bezier paths created using GIMP and exported as SVG. /// </summary> /// <param name="p_startPoint">Point where sprite begins</param> /// <param name="p_path">String of points sprite should follow</param> /// <param name="p_ratio">double value indicating stepping size as a fraction of distance along path to take with each step</param> public BezierSprite(Point p_startPoint, string p_path, double p_ratio) { startPoint = p_startPoint; points = new List <Point>(); points.Add(p_startPoint); points.AddRange(BezierSprite.PointsStringToList(p_path)); if ((points.Count - 1) % 3 != 0) { throw new ArgumentException("The number of points in the path must be divisible by 3"); } segmentIndex = 0; segmentCount = (points.Count - 1) / 3; lengthRatios = new List <LengthRatio>(segmentCount); for (int i = 0; i < segmentCount; i++) { lengthRatios.Add(new LengthRatio()); } FillInSegmentLengths(); FillInEndTangents(); loopCount = 0; remainingLoops = 1; traversalTime = 0.0d; speed = 0; completed = false; ratioStep = lengthRatios[0].ratio; }
/// <summary> /// Trace the path quickly through to determine its length /// </summary> /// <param name="p_points"> /// A <see cref="Point"/> array of 4 points specifying Bezier curve /// </param> /// <returns> /// A <see cref="System.Int32"/> as the length of the given curve /// </returns> static public int CalculateCurveLength(Point [] p_points) { if (p_points.Length != 4) { throw new Exception("p_points array passed to CalculateCurveLength must have exactly 4 items, this one has " + p_points.Length); } PointF pA = p_points[0]; double rtn = 0.0d; // determine what ratio gives up about 1 pixel of distance double accuracyRatio = BezierSprite.CalculateRatioSizeToPixelAccuracy(p_points, 1); // set an incrementer double currentRatio = accuracyRatio; // set a limit for when we reach the end of the path - allow some overshoot double topLimit = 1.0 + accuracyRatio; // traverse path one fraction bit at a time calculating distances between points while (currentRatio < topLimit) { PointF pB = CalculatePoint(p_points, currentRatio); rtn += Math.Sqrt(Math.Pow(pA.X - pB.X, 2) + Math.Pow(pA.Y - pB.Y, 2)); pA = pB; currentRatio += accuracyRatio; } return((int)Math.Round(rtn)); }
/// <summary> /// Determine what ratio amounts to a given pixel distance /// </summary> /// <param name="p_points"> /// A <see cref="Point"/> array of 4 points specifying the Bezier curve to analyze /// </param> /// <param name="p_pixels"> /// A <see cref="System.Int32"/> indicating how many pixels the returned ratio should indicate /// </param> /// <returns> /// A <see cref="System.Double"/> representing the ratio on the curve that amounts to p_pixels distance /// </returns> static public double CalculateRatioSizeToPixelAccuracy(Point [] p_points, int p_pixels) { if (p_points.Length != 4) { throw new Exception("p_points array passed to GetRatioSizeToPixelAccuracy must have exactly 4 items, this one has " + p_points.Length); } double testRatio = 1.0d; PointF prevPoint = BezierSprite.CalculatePoint(p_points, 0.0d); // first cut test ratio in half repeatedly until we fall below the requested // pixel threshold, then increment the ratio back up by .01 until we hit the // threshold again. while (true) { testRatio /= 2.0d; PointF nextPoint = BezierSprite.CalculatePoint(p_points, testRatio); if (BezierSprite.Hypot(prevPoint, nextPoint) < p_pixels) { while (true) { testRatio += 0.01d; PointF nextPoint2 = BezierSprite.CalculatePoint(p_points, testRatio); if (BezierSprite.Hypot(prevPoint, nextPoint2) > p_pixels) { break; } } break; } } return(testRatio); }
/// <summary> /// Returns the sprites next position on the path using the following formula: /// /// Bx(t) = (1-t)^3 x P1x + 3 x (1-t)^2 x t x P2x + 3 x (1-t) x t^2 x P3x + t^3 x P4x /// By(t) = (1-t)^3 x P1y + 3 x (1-t)^2 x t x P2y + 3 x (1-t) x t^2 x P3y + t^3 x P4y /// /// http://upload.wikimedia.org/math/2/d/5/2d5e5d58562d8ec2c35f16df98d2b974.png /// B(t)=(1-t)^2)*p0 + 2(1-t)*t*p1 + t^2(p2) /// quadratic: http://upload.wikimedia.org/wikipedia/commons/b/bf/Bezier_2_big.png /// cubic: http://upload.wikimedia.org/wikipedia/commons/c/c1/Bezier_3_big.png /// </summary> /// <returns>Point indicating next position of sprite</returns> protected override Point GetNewPosition() { currentPoint = BezierSprite.CalculatePoint( points.GetRange(segmentIndex * 3, 4).ToArray(), ratio ); // if approaching end of current curve, then get pre-calculated angle if ((1.0d - ratio) < ratioStep) { tangentAngle = lengthRatios[segmentIndex].endTangent; } else if (ratio < ratioStep) // if just past the start of next curve, then use pre-calculated angle { tangentAngle = lengthRatios[segmentIndex].startTangent; } else // else get angle along the curve based on ratio { tangentAngle = CalculateTangentAngle(points.GetRange(segmentIndex * 3, 4).ToArray(), ratio, currentPoint); } //Console.WriteLine("tangentAngle={0}, seg={1}, ratio={2}", tangentAngle, segmentIndex, ratio); ratio = (double)(ratio + ratioStep); if (ratio > 1.0d) { ratio -= 1.0d; segmentIndex++; if (segmentIndex >= segmentCount) { if (remainingLoops != -1) { if (--remainingLoops > 0) { segmentIndex = 0; } else { completed = true; } } else { segmentIndex = 0; } loopCount++; } else { ratioStep = lengthRatios[segmentIndex].ratio; } } return(new Point((int)Math.Round(currentPoint.X), (int)Math.Round(currentPoint.Y))); }
/// <summary> /// Determine and store the length of each curve segment along the whole path /// /// Assumption: /// points has 3*n+1 points in it. /// /// </summary> private void FillInSegmentLengths() { if (lengthRatios == null) { throw new Exception("lengthRatios must be defined assigned before calling FillInEndTangents"); } pathLength = 0; for (int i = 0; i < segmentCount; i++) { LengthRatio lr = new LengthRatio(); lr.length = BezierSprite.CalculateCurveLength( points.GetRange(i * 3, 4).ToArray() // there are 4 points to each segment. * 3 because we start each next segment with the end point of the previous segment ); lr.ratio = defaultRatio; // default value since we haven't calc'd ratios yet lengthRatios[i] = lr; pathLength += lr.length; // calc length of whole path } }