public CircleArc(Circle2D circle, double startAngle, double sweepAngle) { // For negative sweep angles we need to adjust the start angle so that we can make the sweep // angle positive and be representing the same arc. if (sweepAngle < 0.0) { startAngle += sweepAngle; sweepAngle = -sweepAngle; } // Get the start angle in the range [0, 2*pi) if (startAngle < 0.0) { startAngle = (startAngle % c_twoPi) + c_twoPi; } else { startAngle = startAngle % c_twoPi; } // Get the sweep angle in the range [0, 2*pi). Note that it's non-negative at this point. sweepAngle %= c_twoPi; m_circle = circle; m_startAngle = startAngle; m_sweepAngle = sweepAngle; }
public static void Swap(ref Circle2D a, ref Circle2D b) { Circle2D temp = a; a = b; b = temp; }
public unsafe void FillCircle(SimpleImage img, uint clr, Circle2D circle) { // Optimization: For composite-blending, we don't have do draw anything if the // drawing color has an alpha value of zero if (0 == (clr & 0xFF000000)) { return; } // Soften radius (hard-coded for now) float sr = 2.0f; // Compute the inclusive y-starting point int ystart = (int)Math.Floor(circle.MinY); // Compute the inclusive y-ending point int yend = (int)Math.Ceiling(circle.MaxY); // Do a quick check to see if we're completely off the image if (yend < 0 || ystart >= img.Height) { return; } // Constrain y-values, if needed if (ystart < 0) { ystart = 0; } if (yend > img.Height - 1) { yend = img.Height - 1; } // Compute a float alpha value in the range [0.0, 1.0] float alpha = (float)(clr >> 24) / 255.0f; // Loop through rows of pixels for (; ystart <= yend; ystart++) { // Compute intersections for this row float minx, maxx; circle.HLineIntersection((float)ystart, out minx, out maxx); // Skip to the next row if there was no intersection if (float.IsNaN(minx) || float.IsNaN(maxx)) { continue; } // Compute the starting point on the left edge of the circle int xLeft = (int)Math.Floor(minx); // Skip this row if it's off the image if (xLeft >= img.Width) { continue; } // Constrain if necessary if (xLeft < 0) { xLeft = 0; } // Now compute the starting point on the right edge of the circle int xRight = (int)Math.Ceiling(maxx); // Skip if we're completely off the image if (xRight < 0) { continue; } // Constrain, if necessary if (xRight >= img.Width) { xRight = img.Width - 1; } // Make a pointer to this row of pixels uint* row = &img.Pixels[ystart * img.Width + xLeft]; // Draw from the left edge until we hit a point where we're no longer within // the softening radius distance from the edge while (xLeft <= xRight) { float dist = circle.DistanceFromCenter((float)xLeft, (float)ystart); if (dist >= circle.Radius) { // Advance to next pixel row++; xLeft++; continue; } if (dist <= circle.Radius - sr) { break; } // Compute the alpha value float newAFloat = (circle.Radius - dist) / sr * alpha * 255.0f; // Blend BlendComposite((clr & 0x00FFffFF) | (((uint)newAFloat) << 24), row); // Advance to next pixel row++; xLeft++; // See if we end up heading off the right edge of the image if (xLeft >= img.Width) { break; } } // If we drew pixels until we went off the image, then are done with this row if (xLeft >= img.Width) { continue; } // It's possible, depending on the softening radius and which part of the circle we're // rendering, that the entire row has already been processed. Remember that xLeft at // this point is one to the right of the last pixel that was processed. if (xRight < xLeft) { continue; } // Set the pointer to where we're starting row = &img.Pixels[ystart * img.Width + xRight]; // Draw from the right edge until we hit a point where we're: // a) no longer within the softening radius distance from the edge // b) we're off the image (xRight < 0) // or // c) we're less than xLeft while (xRight >= 0) { float dist = circle.DistanceFromCenter((float)xRight, (float)ystart); if (dist >= circle.Radius) { // Advance to next pixel row--; xRight--; continue; } if (dist <= circle.Radius - sr) { break; } // Compute the alpha value float newAFloat = (circle.Radius - dist) / sr * alpha * 255.0f; // Blend BlendComposite((clr & 0x00FFffFF) | (((uint)newAFloat) << 24), row); // Advance to next pixel row--; xRight--; if (xRight < xLeft) { break; } } // At this point we've processed from the left and right edges of the circle // on this row and have taken care of all pixels that need edge softening. All // that remains is filling pixels in between where we left off on the left and // right. if (xRight >= xLeft) { DrawHorizontalLine(xLeft, xRight, ystart, clr, img.Pixels, img.Width, img.Height, BlendMode.AlphaComposite); } } }
// TODO: Make this obey blending rules properly public unsafe void DrawPieChart(uint* pxls, int width, int height, PieChartSection[] sections, uint color) { if (0 == sections.Length) { throw new ArgumentException("Pie chart sections array cannot be empty"); } // We need at least a 5x5 image if (width < 5 || height < 5) { return; } // If the image is not square, we want non-zero starting points int ystart = 0; int yend = height - 1; // Inclusive upper bound if (height > width) { ystart = (height - width) / 2; yend = height - ystart; } float xcenter = (float)width / 2.0f; float ycenter = (float)height / 2.0f; double radius = Math.Min(xcenter, ycenter) - 2.0; // Make a circle object for the pie chart area Circle2D circle = new Circle2D(xcenter, ycenter, (float)radius); // Compute the sum of all values in the pie sections double sum = 0.0; foreach (PieChartSection pcs in sections) { sum += pcs.Value; } // Make an angle table. This table stores the ending angle of each category. Also // make a color table. double[] angles = new double[sections.Length]; uint[] clrLookup = new uint[sections.Length]; int i = 0; double angle = 0.0; foreach (PieChartSection pcs in sections) { clrLookup[i] = pcs.Color; angles[i] = pcs.Value / sum * (Math.PI * 2.0) + angle; angle = pcs.Value / sum * (Math.PI * 2.0) + angle; i++; } while (ystart <= yend) { // Compute the intersections of the scan line with the pie chart float minx, maxx; circle.HLineIntersection((float)ystart, out minx, out maxx); // Clear the row if there isn't an intersection if (float.IsNaN(minx) || float.IsNaN(maxx)) { DrawHorizontalLine(0, width - 1, ystart, 0, pxls, width, height, BlendMode.None); ystart++; continue; } int xstart = (int)Math.Floor(minx); int xend = (int)Math.Min(Math.Ceiling(maxx), (float)(width - 1)); uint* row = &pxls[ystart * width + xstart]; for (int x = xstart; x <= xend; x++) { // For each pixel, we want to start by computing the distance from the center Vector2D pos = new Vector2D((float)x - xcenter, ycenter - (float)ystart); double dist = pos.Length; // Set the pixel to 0 if we're outside of the pie chart area if (dist >= radius) { *row++ = 0; continue; } // Compute the angle so we know what category of the pie we're in angle = pos.GetCCWAngle(); i = 0; while (angle > angles[i]) { i++; } // Advance to the next pixel *row++ = clrLookup[i]; } ystart++; } // Draw lines between sections if (sections.Length > 1) { List<Stadium> stadiums = new List<Stadium>(); foreach (double tempAngle in angles) { stadiums.Add(new Stadium(circle.Center, new Vector2D(xcenter + (float)(radius * Math.Cos(tempAngle)), ycenter - (float)(radius * Math.Sin(tempAngle))), 1.75f)); } FillStadiums(new SimpleImage(width, height, pxls), color, stadiums.ToArray(), 2f); } // Draw a circular border around the pie chart FrameCircle_Composite(circle, 3.5f, color, pxls, width, height); }
/// <summary> /// Draws an arc using alpha-compositing as the blend mode. Currently uses a hard-coded softening /// distance value of 1.5 pixels. /// </summary> /// <param name="startAngle">Start angle, in radians</param> /// <param name="sweepAngle">Sweep angle, in radians</param> public unsafe void DrawArc(Circle2D circle, float lineThickness, double startAngle, double sweepAngle, uint color, uint* pixels, int imageWidth, int imageHeight) { // Check parameters if (lineThickness <= 0.0f) { throw new ArgumentException( "Line thickness value must be a positive number"); } // If the circle is degenerate then we have nothing to render if (circle.IsDegenerate) { return; } // Special case: if the sweep angle is >= 2*pi then we just frame the whole circle if (sweepAngle >= c_twoPi) { FrameCircle(circle, lineThickness, color, pixels, imageWidth, imageHeight); return; } // Softening radius (hard-coded for now) (change method summary if this changes) float sr = 1.5f; // Compute lineThickness / 2 because it's used a lot float lto2 = lineThickness / 2.0f; double colorAlphaDouble = ((double)(color >> 24)) / 255.0; // Another special case if the line is thick enough to extend in and past the center of the circle if (circle.Radius - lto2 <= 0.0f) { FillCircle(new SimpleImage(imageWidth, imageHeight, pixels), color, circle); return; } // Build the "outer" circle Circle2D outer = new Circle2D(circle.Center, circle.Radius + lto2); // Build the "inner" circle Circle2D inner = new Circle2D(circle.Center, circle.Radius - lto2); // Build the arc object CircleArc ca = new CircleArc(circle, startAngle, sweepAngle); // Find out the starting y-value int ystart = (int)Math.Floor(ca.MinY - lto2); // If it's below the bottom of the image then we have nothing to render if (ystart >= imageHeight) { return; } // Make sure it's not < 0 if (ystart < 0) { ystart = 0; } // Now determine the inclusive ending y-value int yend = (int)Math.Ceiling(ca.MaxY + lto2); if (yend < 0) {return;} if (yend >= imageHeight) { yend = imageHeight - 1; } // Loop through rows of pixels and render while (ystart++ <= yend) { // Compute outer circle intersections float ocx1, ocx2; outer.HLineIntersection((float)ystart, out ocx1, out ocx2); // If there's no intersection or the two x-values are equal, then move onto // the next y-value if (float.IsNaN(ocx1) || float.IsNaN(ocx2) || ocx1 == ocx2) { continue; } // Compute the inner circle intersections float icx1, icx2; inner.HLineIntersection((float)ystart, out icx1, out icx2); float[] xvals; if (float.IsNaN(icx1) || float.IsNaN(icx2) || icx1 == icx2) { // If there's no inner circle intersections then our array just has the two // x-values for the outer intersection. xvals = new float[] { ocx1, ocx2 }; } else { xvals = new float[] { ocx1, icx1, icx2, ocx2 }; } // Render between pairs of intersection points for (int i = 0; i < xvals.Length; i+=2) { int x1 = (int)Math.Floor(xvals[i]); if (x1 >= imageWidth) { continue; } if (x1 < 0) { x1 = 0; } int x2 = (int)Math.Ceiling(xvals[i + 1]); if (x2 >= imageWidth) { x2 = imageWidth - 1; } uint* start = &pixels[ystart * imageWidth + x1]; uint* end = &pixels[ystart * imageWidth + x2]; while (start++ <= end) { float dist = ca.DistanceU(new Vector2D((float)x1, (float)ystart)); if (dist >= lto2) { x1++; continue; } else if (dist > (lto2 - sr)) { double alpha = (lto2 - dist) / sr; alpha *= colorAlphaDouble; uint clr = color & 0x00FFffFF; clr |= (((uint)(alpha * 255.0)) << 24); BlendComposite(clr, start); } else { BlendComposite(color, start); } x1++; } } } }
protected unsafe void FrameCircle_Composite(Circle2D circle, float frameThickness, uint clr, uint* pxls, int w, int h) { // Softening radius (hard-coded for now) float sr = 2.0f; // Handle case where the frame is so thick that we just fill the whole circle if (frameThickness / 2.0f >= circle.Radius + sr) { Circle2D newCircle = new Circle2D(circle.Center, circle.Radius + frameThickness / 2f); FillCircle(new SimpleImage(w, h, pxls), clr, circle); return; } // First make the outer and inner circle objects Circle2D outer = new Circle2D(circle.Center, circle.Radius + frameThickness / 2.0f); Circle2D inner = new Circle2D(circle.Center, circle.Radius - frameThickness / 2.0f); int y = (int)Math.Max(0.0f, Math.Floor(outer.MinY)); for (; y < h; y++) { // Compute the x-intersection points for this y-value float outMin, outMax, inMin, inMax; outer.HLineIntersection((float)y, out outMin, out outMax); inner.HLineIntersection((float)y, out inMin, out inMax); // If there's not a valid intersection, then skip this row. Note that there // could be an intersection with the outer circle and not the inner, but not // the other way around. if (float.IsNaN(outMin) || float.IsNaN(outMax)) { continue; } float[] ranges; if (float.IsNaN(inMin) || float.IsNaN(inMax)) { // This means there's an intersection with the outer circle and not the // inner. In this case we only have one continuous range of pixels to // process for this row. // Check to see if we're off the image if (outMax < 0.0f || outMin > (float)(w - 1)) { continue; } ranges = new float[] { outMin, outMax }; } else { ranges = new float[] { outMin, inMin, inMax, outMax }; } // Process the pixel ranges for (int i = 0; i < ranges.Length; i += 2) { int x = (int)Math.Max(0.0f, Math.Floor(ranges[i])); int maxx = (int)Math.Min((float)(w - 1), Math.Ceiling(ranges[i + 1])); // Make a pointer to the pixel where we start rendering uint* row = &pxls[y * w + x]; while (x <= maxx) { // Compute the distance from this pixel to the edge of the circle and // soften if needed float dist = circle.DistanceFromCenter(new Vector2D(x, y)); if (dist >= outer.Radius || dist <= inner.Radius) { x++; row++; continue; } if ((outer.Radius - dist) <= sr) { float newA = ((outer.Radius - dist) / sr) * 255.0f; BlendComposite((clr & 0x00FFffFF) | (((uint)newA) << 24), row); } else if ((dist - inner.Radius) <= sr) { float newA = ((dist - inner.Radius) / sr) * 255.0f; BlendComposite((clr & 0x00FFffFF) | (((uint)newA) << 24), row); } else { BlendComposite(clr, row); } x++; row++; } } } }
public unsafe void FrameCircle(Circle2D circle, float frameThickness, uint clr, uint* pxls, int imgWidth, int imgHeight) { FrameCircle_Composite(circle, frameThickness, clr, pxls, imgWidth, imgHeight); }