How to scale and draw a plot in C# - c#

I am trying to use the CSV below for drawing a plot:
2.364258,3.005366
2.723633,3.009784
3.083008,3.012145
3.442383,3.012705
3.801758,3.010412
4.160156,3.010703
4.518555,3.011985
4.876953,3.012547
5.235352,3.009941
5.592773,3.011252
5.951172,3.010596
6.30957,3.011951
6.667969,3.010613
7.026367,3.008634
7.384766,3.009744
7.743164,3.01062
8.101563,3.00942
8.459961,3.009438
8.818359,3.009478
9.177734,3.010827
What I did so far is that I tried to make a Class to do this! this is the part when I try to draw the curve:
class Plotter
{
#region Fields and variables
private Bitmap plot;
private Graphics g;
public string PlotType {get; set;}
private int iWidth; //Width of the box
private int iHeight; //
private float xMax; //maximum range on X axis
private float yMax; //maximum range on Y axis
private PointF[] points;
#endregion
#region Constructors
/// <summary>
/// Constructor of class
/// </summary>
/// <param name="iWidth">Width of image in pixels</param>
/// <param name="iHeight">Height of image in pixels</param>
/// <param name="xMax">Maximum value of the values on X</param>
/// <param name="yMax">Maximum value of the values on Y</param>
/// <param name="pairs">Pairs of data in an array of PointF[] this is raw data!!</param>
public Plotter(int iWidth, int iHeight, float xMax, float yMax, PointF[] points)
{
this.iWidth = iWidth;
this.iHeight = iHeight;
this.xMax = xMax;
this.yMax = yMax;
this.points = points;
plot = new Bitmap(iWidth, iHeight);
}
public Bitmap DrawPlot()
{
Pen blackPen = new Pen(Color.Black, 1);
g = Graphics.FromImage(plot);
PointF[] p = new PointF[points.GetLength(0)];
//Try to scale input data to pixel coordinates
foreach (PointF point in points)
{
int i = 0;
p[i].X = point.X * iWidth;
p[1].X = point.Y * iHeight;
}
g.DrawCurve(blackPen, p, 0);
return plot;
}
What I get at the end is jsut a stright line! that I think has been drawn on X{0,0} and Y{0,0} to X{0, 400} and Y{0,0}
Can you help me correct the mistakes please?
P.S: http://itools.subhashbose.com/grapher/index.php this site can draw the plot I need pretty good from the CSV data I have (if you need to check).
Thanks!

This seems to be your problem:
foreach (PointF point in points)
{
int i = 0;
p[i].X = point.X * iWidth;
p[1].X = point.Y * iHeight;
}
i is always zero and you are never assigning Y. The "second" assignment isn't even using i, but the 1 index.
Quick fix without error checking:
int i = 0;
foreach (PointF point in points)
{
p[i].X = point.X * iWidth;
p[i].Y = point.Y * iHeight;
i++;
}

Your assigning the x both times.
p[i].X = point.X * iWidth;
p[1].X = point.Y * iHeight;
And as #LarsTech points out you need to fix the counter

Related

Find point on ellipse given the angle, then reverse the process

My first task is simple: find the points of the ellipse to be drawn on screen. I made an Ellipse class below with a method that takes in an angle between 0 to 2*PI and returns the point.
public class Ellipse
{
public PointF Center { get; set; }
public float A { get; set; } /* horizontal semiaxis */
public float B { get; set; } /* vertical semiaxis */
public Ellipse(PointF center, float a, float b)
{
this.Center = center;
this.A = a;
this.B = b;
}
public PointF GetXYWhenT(float t_rad)
{
float x = this.Center.X + (this.A * (float)Math.Cos(t_rad));
float y = this.Center.Y + (this.B * (float)Math.Sin(t_rad));
return new PointF(x, y);
}
}
I use the parametric equation of the ellipse as it is convenient for this task. Parameter t is the angle. X and Y values are calculated and put together as a point on the ellipse. By increasing parameter t, I can obtain the points in the order that makes drawing the ellipse as simple as connecting the dots.
private void RunTest1()
{
PointF center = new PointF(0, 0);
float a = 3; /* horizontal semiaxis */
float b = 4; /* vertical semiaxis */
Ellipse ellipse = new Ellipse(center, a, b);
List<PointF> curve = new List<PointF>(); /* collects all points needed to draw the ellipse */
float start = 0;
float end = (float)(2 * Math.PI); /* 360 degrees */
float step = 0.0174533f; /* 1 degree */
for (float t_rad = start; t_rad <= end; t_rad += step)
{
PointF point = ellipse.GetXYWhenT(t_rad);
curve.Add(point);
}
}
RunTestX are methods I run in Main. The first will give me the points I need to draw this ellipse. The points are correct. I have visual confirmation of the ellipse being drawn to specification using a draw method that I will not be including here. Drawing it is NOT the issue. The takeaway here is that for every value of t_rad, I have a corresponding point on the curve.
Now I need to perform a different task after I draw the ellipse. To do this, I need to reverse the process in that I take an arbitrary point on the ellipse and convert it back to the t_rad. Math.Atan2 should do the trick. The method is called GetTWhenPoint. It's an extension method in MyMath class.
public static class MyMath
{
public static float GetTWhenPoint(this PointF center, PointF point)
{
float x = point.X - center.X;
float y = point.Y - center.Y;
float retval = (float)Math.Atan2(y, x);
if (retval < 0)
{
retval += (float)(2 * Math.PI);
}
return retval;
}
}
Simple trigonometry, right? However...
private void RunTest2()
{
PointF center = new PointF(0, 0);
float a = 3; /* horizontal semiaxis */
float b = 4; /* vertical semiaxis */
Ellipse ellipse = new Ellipse(center, a, b);
string debug = "TEST 2\r\n";
float start = 0;
float end = (float)(2 * Math.PI);
float step = 0.0174533f;
for (float t_rad = start; t_rad <= end; t_rad += step)
{
PointF point = ellipse.GetXYWhenT(t_rad);
double t_rad2 = center.GetTWhenPoint(point);
debug += t_rad.ToString() + "\t" + t_rad2.ToString() + "\r\n";
}
Clipboard.SetText(debug);
}
When I use it to convert the point back to t_rad2, I expect it to be equal to or pretty darn close to the original t_rad.
TEST 2
0 0
0.0174533 0.0232692267745733
0.0349066 0.0465274415910244
0.0523599 0.0697636753320694
0.0698132 0.0929670184850693
0.0872665 0.116126760840416
...
6.178444 6.14392471313477
6.195897 6.1670298576355
6.21335 6.19018936157227
6.230803 6.21339273452759
6.248257 6.23662853240967
6.26571 6.25988674163818
6.283163 6.28315591812134
What am I missing here? All my numbers so far have been in radians (as far as I can tell). Now here's where it gets weirder...
private void RunTest3()
{
PointF center = new PointF(0, 0);
float a = 4; /* horizontal semiaxis */
float b = 4; /* vertical semiaxis */
Ellipse ellipse = new Ellipse(center, a, b);
string debug = "TEST 3\r\n";
float start = 0;
float end = (float)(2 * Math.PI);
float step = 0.0174533f;
for (float t_rad = start; t_rad <= end; t_rad += step)
{
PointF point = ellipse.GetXYWhenT(t_rad);
double t_rad2 = center.GetTWhenPoint(point);
debug += t_rad.ToString() + "\t" + t_rad2.ToString() + "\r\n";
}
Clipboard.SetText(debug);
}
If I set a and b equal to make the ellipse a perfect circle then everything looks normal!
TEST 3
0 0
0.0174533 0.0174532998353243
0.0349066 0.0349065996706486
0.0523599 0.0523599050939083
0.0698132 0.0698131918907166
0.0872665 0.0872664898633957
...
6.178444 6.17844390869141
6.195897 6.19589710235596
6.21335 6.21335029602051
6.230803 6.23080348968506
6.248257 6.24825668334961
6.26571 6.26570987701416
6.283163 6.28316307067871
What this tells me is that when I convert the point back to t_rad2 it is somehow affected by the dimensions of the ellipse. But how? Apart from the center adjustment of the ellipse with respect to the Cartesian origin (0,0), GetTWhenPoint method does not make use of any other information from the Ellipse class specifically the semi-axes. Math.Atan2 only needs the x and y values of the point to find the angle it makes with the 0-degree vector. That's basic trigonometry.
It shouldn't even care that it's a point on the ellipse. From the context of the method, it's just a point just like infinitely any other. How is my extension method somehow being affected by the dimensions of my ellipse?
Is it my math that's wrong? I mean it's been a while since I used trig but I think I remember the simple ones correctly.
Thanks in advance!
I think this is what you want.
public class Ellipse
{
public PointF Center { get; set; }
public float A { get; set; } /* horizontal semiaxis */
public float B { get; set; } /* vertical semiaxis */
public Ellipse(PointF center, float a, float b)
{
this.Center=center;
this.A=a;
this.B=b;
}
public PointF GetXYWhenT(float t_rad)
{
float x = this.Center.X+(this.A*(float)Math.Cos(t_rad));
float y = this.Center.Y+(this.B*(float)Math.Sin(t_rad));
return new PointF(x, y);
}
public float GetParameterFromPoint(PointF point)
{
var x = point.X-Center.X;
var y = point.Y-Center.Y;
// Since x=a*cos(t) and y=b*sin(t), then
// tan(t) = sin(t)/cos(t) = (y/b) / (x/a)
return (float)Math.Atan2(A*y, B*x);
}
}
class Program
{
static readonly Random rng = new Random();
static void Main(string[] args)
{
var center = new PointF(35.5f, -12.2f);
var ellipse = new Ellipse(center, 18f, 44f);
// Get t between -π and +π
var t = (float)(2*Math.PI*rng.NextDouble()-Math.PI);
var point = ellipse.GetXYWhenT(t);
var t_check = ellipse.GetParameterFromPoint(point);
Debug.WriteLine($"t={t}, t_check={t_check}");
// t=-0.7434262, t_check=-0.7434263
}
}
I would consider the proper parametrization of a curve as one with a parameter that spans between 0 and 1. Hence the need to specify radians goes away
x = A*Cos(2*Math.PI*t)
y = B*Sin(2*Math.PI*t)
and the reverse
t = Atan2(A*y, B*x)/(2*PI)
Also, consider what the ellipse looks like in polar coordinates relative to the center.
x = A*Cos(t) = R*Cos(θ) | TAN(θ) = B/A*TAN(t)
y = B*Sin(t) = R*Sin(θ) |
| R = Sqrt(B^2+(A^2-B^2)*Cos(t)^2)
A*B
R(θ) = ----------------------------
Sqrt(A^2+(B^2-A^2)*Cos(θ)^2)
Also, consider the following helper functions that wrap angles around the desired quadrants (radian versions)
/// <summary>
/// Wraps angle between 0 and 2π
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapTo2PI(this double angle)
=> angle-(2*Math.PI)*Math.Floor(angle/(2*Math.PI));
/// <summary>
/// Wraps angle between -π and π
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapBetweenPI(this double angle)
=> angle+(2*Math.PI)*Math.Floor((Math.PI-angle)/(2*Math.PI));
and the degree versions
/// <summary>
/// Wraps angle between 0 and 360
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapTo360(this double angle)
=> angle-360*Math.Floor(angle/360);
/// <summary>
/// Wraps angle between -180 and 180
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
/// <remarks>see: http://stackoverflow.com/questions/7271527/inconsistency-with-math-round</remarks>
public static double WrapBetween180(this double angle)
=> angle+360*Math.Floor((180-angle)/360);

Determine if a point is inside a 4-winged star shape via it's formula extracted from the drawing algorithm

This code draws a star shape :
public void draw(double radius, Color color)
{
int x = 0, y = Convert.ToInt32(radius);
double d = (5 / 4) - radius;
circlePoint(x, y, color);
while (x < y)
{
if (d < 0)
{
d += Math.Pow(x, 2) + 3;
x++;
y--;
}
else
{
d += (x - y) * 2 + 5;
y--;
}
circlePoint(x, y, color);
}
}
The drawn shape is shown below
The Actual Question:
I want to write a method (bool hasPoint(Point p) ) to check if p is inside this shape. I know for other shapes like a circle or an ellipse, we can check the point's x and y in relative to the object's formula. My Question is "How can I find the formula using the algorithm in draw() ?
Might be helpful:
I was messing around with the formula of circle and ellipse and I encountered this shape. It's obvious that it has a relation with those shapes formula.
EDIT:
This is not a polygon. You can see this as a deformed circle (an ellipse actually). In a circle, if p.X-x0^2 + p.Y-y0^2 is less than radius^2, then p is a point inside. You can easily tell that it is the formula of a cirlce: x^2 + y^2 = r^2 .
EDIT 2:
( (x0,y0) is the center point )
void circlePoint(int x, int y, Color foreColor)
{
putPixel(x + x0, y + y0);
putPixel(y + x0, x + y0);
putPixel(-x + x0, y + y0);
putPixel(-y + x0, x + y0);
putPixel(x + x0, -y + y0);
putPixel(y + x0, -x + y0);
putPixel(-x + x0, -y + y0);
putPixel(-y + x0, -x + y0);
}
void putPixel(int x, int y, Color color){ bitmap.SetPixel(x, y, color); }
EDIT3:
It appears that this curve is mirrored by 8 lines (2 horizontal, 2 vertical and 4 diagonal):
EDIT4:
Based on emrgee's answer I've added these 2 functions, I don't know where I am wrong but it seems hasPointInside() never returns true;
public double contour(double x)
{
double x2 = Math.Pow(x,2);
return -0.190983 + 0.427051 * Math.Sqrt(0.923607 + (0.647214 * x) - (1.37082 * x2));
}
public bool hasPointInside(Point p)
{
int min = Math.Min(Math.Abs(p.X), Math.Abs(p.Y));
int max = Math.Max(Math.Abs(p.X), Math.Abs(p.Y));
return min <= radius * contour(max / radius);
}
The draw method computes the star contour iteratively, starting from its tip at (0, radius). For understanding and maintaining the code, as well as for the point-in-star test you need, a closed formula of the contour would be preferable. Trying to extract such a formula from the code, let's first analyze the true branch of the if, which is the only branch taken for the first few iterations. The accumulation of d can be transformed into a closed form for d:
While this already doesn't look like anything related to an ellipse, d gets accumulated with a different formula when in the else branch. I claim it is not possible to transform d into a closed form for the entire algorithm.
Other observations: the slope of the contour can only get -1 or steeper. And there are magic numbers in the formulas not computed from the radius argument. So there are strong indications that the contour resembles an ellipse only accidentally.
Now let's assume you want a star with an ellipsoid contour, with 90° tips and with 90° corners. Let's start with the blue ellipse with major radius 1 in x and minor radius 1/2 in y direction:
We scale and move it so that the green point ends up on the diagonal and the red point, where the slope is -1, ends up at (1,0). With a bit of formula massage, the contour becomes:
or, numerically:
The offset on the x axis is -2 + Sqrt(5) or 0.236068.
Pseudo code for your hasPoint method would then look like this:
with this result for radius 30:
In C#, you would provide these methods:
/// <summary>
/// Calculates a contour point of the four-pointed star.
/// </summary>
/// <param name="x">The abscissa of the contour point.</param>
/// <returns>Returns the ordinate of the contour point.</returns>
private static double StarContour(double x)
{
return -0.190983d + (0.427051d * Math.Sqrt(0.923607d + (0.647214d * x) - (1.37082d * x * x)));
}
/// <summary>
/// Tests whether a point is inside the four-pointed star of the given radius.
/// </summary>
/// <param name="radius">The radius of the star.</param>
/// <param name="x">The abscissa of the point to test.</param>
/// <param name="y">The ordinate of the point to test.</param>
/// <returns>Returns <see langword="true" /> if the point (<paramref name="x" />, <paramref name="y" />
/// is inside or on the contour of the star with the given <paramref name="radius" />;
/// otherwise, if the point is outside the star, returns <see langword="false" />.</returns>
private static bool InFourStar(double radius, double x, double y)
{
double min = Math.Abs(x);
double max = Math.Abs(y);
if (min > max)
{
// swap min and max
double h = min;
min = max;
max = h;
}
return min <= radius * StarContour(max / radius);
}
and then use them like this:
/// <summary>
/// Draws the outline of a four-pointed star of given radius and color.
/// </summary>
/// <param name="radius">The radius of the star.</param>
/// <param name="color">The color of the outline.</param>
private void DrawFourStar(double radius, Color color)
{
const double Offset = 0.236068d;
for (int x = (int)(Offset * radius); x < radius; ++x)
{
int y = (int)(radius * StarContour(x / radius));
this.CirclePoint(x, y, color);
}
}
/// <summary>
/// Draws sample points inside or on a four-pointed star contour of given radius.
/// </summary>
/// <param name="radius">The radius of the star.</param>
/// <param name="color">The color of the sample points.</param>
private void DrawSamplesInStar(double radius, Color color)
{
const int SamplingStep = 10;
for (int x = (int)(-radius); x < radius; x += SamplingStep)
{
for (int y = (int)(-radius); y < radius; y += SamplingStep)
{
if (InFourStar(radius, x, y))
{
this.bitmap.SetPixel(x + this.x0, y + this.y0, color);
}
}
}
}
which looks like this for radius 200:

Split resize algorithm into two passes

I have written the following resizing algorithm which can correctly scale an image up or down. It's far too slow though due to the inner iteration through the array of weights on each loop.
I'm fairly certain I should be able to split out the algorithm into two passes much like you would with a two pass Gaussian blur which would vastly reduce the operational complexity and speed up performance. Unfortunately I can't get it to work. Would anyone be able to help?
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
Weight[] verticalValues = this.verticalWeights[y].Values;
for (int x = startX; x < endX; x++)
{
Weight[] horizontalValues = this.horizontalWeights[x].Values;
// Destination color components
Color destination = new Color();
// This is where there is too much operation complexity.
foreach (Weight yw in verticalValues)
{
int originY = yw.Index;
foreach (Weight xw in horizontalValues)
{
int originX = xw.Index;
Color sourceColor = Color.Expand(source[originX, originY]);
float weight = yw.Value * xw.Value;
destination += sourceColor * weight;
}
}
destination = Color.Compress(destination);
target[x, y] = destination;
}
}
});
Weights and indices are calculated as follows. One for each dimension:
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="destinationSize">The destination section size.</param>
/// <param name="sourceSize">The source section size.</param>
/// <returns>
/// The <see cref="T:Weights[]"/>.
/// </returns>
private Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
{
IResampler sampler = this.Sampler;
float ratio = sourceSize / (float)destinationSize;
float scale = ratio;
// When shrinking, broaden the effective kernel support so that we still
// visit every source pixel.
if (scale < 1)
{
scale = 1;
}
float scaledRadius = (float)Math.Ceiling(scale * sampler.Radius);
Weights[] result = new Weights[destinationSize];
// Make the weights slices, one source for each column or row.
Parallel.For(
0,
destinationSize,
i =>
{
float center = ((i + .5f) * ratio) - 0.5f;
int start = (int)Math.Ceiling(center - scaledRadius);
if (start < 0)
{
start = 0;
}
int end = (int)Math.Floor(center + scaledRadius);
if (end > sourceSize)
{
end = sourceSize;
if (end < start)
{
end = start;
}
}
float sum = 0;
result[i] = new Weights();
List<Weight> builder = new List<Weight>();
for (int a = start; a < end; a++)
{
float w = sampler.GetValue((a - center) / scale);
if (w < 0 || w > 0)
{
sum += w;
builder.Add(new Weight(a, w));
}
}
// Normalise the values
if (sum > 0 || sum < 0)
{
builder.ForEach(w => w.Value /= sum);
}
result[i].Values = builder.ToArray();
result[i].Sum = sum;
});
return result;
}
/// <summary>
/// Represents the weight to be added to a scaled pixel.
/// </summary>
protected class Weight
{
/// <summary>
/// The pixel index.
/// </summary>
public readonly int Index;
/// <summary>
/// Initializes a new instance of the <see cref="Weight"/> class.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="value">The value.</param>
public Weight(int index, float value)
{
this.Index = index;
this.Value = value;
}
/// <summary>
/// Gets or sets the result of the interpolation algorithm.
/// </summary>
public float Value { get; set; }
}
/// <summary>
/// Represents a collection of weights and their sum.
/// </summary>
protected class Weights
{
/// <summary>
/// Gets or sets the values.
/// </summary>
public Weight[] Values { get; set; }
/// <summary>
/// Gets or sets the sum.
/// </summary>
public float Sum { get; set; }
}
Each IResampler provides the appropriate series of weights based on the given index. the bicubic resampler works as follows.
/// <summary>
/// The function implements the bicubic kernel algorithm W(x) as described on
/// <see href="https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm">Wikipedia</see>
/// A commonly used algorithm within imageprocessing that preserves sharpness better than triangle interpolation.
/// </summary>
public class BicubicResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
// The coefficient.
float a = -0.5f;
if (x < 0)
{
x = -x;
}
float result = 0;
if (x <= 1)
{
result = (((1.5f * x) - 2.5f) * x * x) + 1;
}
else if (x < 2)
{
result = (((((a * x) + 2.5f) * x) - 4) * x) + 2;
}
return result;
}
}
Here's an example of an image resized by the existing algorithm. The output is correct (note the silvery sheen is preserved).
Original image
Image halved in size using the bicubic resampler.
The code is part of a much larger library that I am writing to add image processing to corefx.
Judging from an abstract point of view (not knowing much about image manipulation) I think it looks like you're computing the values for weight and sourcecolor (in the innermost foreach loop) multiple times (whenever the same pair of indices crops up again); would it be feasible to simply precompute them in advance?
You would need to compute a 'direct product' Matrix for your HorizontalWeight and VerticalWeight matrices (simply multiplying the values for each pair of indices (x, y)) and could also apply Color.Expand to the source in advance.
These tasks can be done in parallel and the 'direct product' (sorry, I don't know the correct Name for that beast) should be available in lots of libraries.
You can try a weighted voronoi diagram. Try randomly a set of points and compute the voronoi diagram. Smooth the polygons with Lloyd's algorithm and interpolate the color of the polygon. With the weight compute the weighted voronoi diagram. For example voronoi stippling and mosaic:http://www.mrl.nyu.edu/~ajsecord/stipples.html and http://www.evilmadscientist.com/2012/stipplegen-weighted-voronoi-stippling-and-tsp-paths-in-processing/.
Ok so here's how I went about it.
The trick is to first resize only the width of the image keeping the height the same as the original image. We store the resultant pixels in a temporary image.
Then it's a case of resizing that image down to our final output.
As you can see we are no longer iterating through both weight collections on each pixel. Despite having to iterate though the outer pixel loop twice the algorithm was much faster in it's operation averaging around 25% faster on my test images.
// Interpolate the image using the calculated weights.
// First process the columns.
Parallel.For(
0,
sourceBottom,
y =>
{
for (int x = startX; x < endX; x++)
{
Weight[] horizontalValues = this.HorizontalWeights[x].Values;
// Destination color components
Color destination = new Color();
foreach (Weight xw in horizontalValues)
{
int originX = xw.Index;
Color sourceColor = Color.Expand(source[originX, y]);
destination += sourceColor * xw.Value;
}
destination = Color.Compress(destination);
this.firstPass[x, y] = destination;
}
});
// Now process the rows.
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
Weight[] verticalValues = this.VerticalWeights[y].Values;
for (int x = startX; x < endX; x++)
{
// Destination color components
Color destination = new Color();
foreach (Weight yw in verticalValues)
{
int originY = yw.Index;
int originX = x;
Color sourceColor = Color.Expand(this.firstPass[originX, originY]);
destination += sourceColor * yw.Value;
}
destination = Color.Compress(destination);
target[x, y] = destination;
}
}
});

Drawing a straight line from points

I am trying to make a simple chart application and I have stumbled upon a problem. I have an array of points which represent a 45 degree line (y = x) of 100 points. I then try to draw the line by converting each point's value to its pixel value and then draw lines from one point to another using graphic.DrawLines().
The the basic code:
struct MyPoint
{
double X { set; get; }
double Y { set; get; }
}
List<MyPoint> Values;
List<System.Drawing.Point> Points;
for (int i = 0; i < Values.Count; i++)
{
// convert point value to pixel coordinate
int x = (int)((Values[i].X - MinimumX) * (double)Control.Width / (MaximumX - MinimumX) + 0.5);
int y = (int)((Values[i].Y - MinimumY) * (double)Control.Height / (MaximumY - MinimumY)) + 0.5;
Points.Add(new System.Drawing.Point(x, y));
}
graphic.DrawLines(pen, Points.ToArray());
Note: MaximumX and MaximumY are 100 (number of points) and minimums are 0.
The thing is that this kind of line is not very straight :) So my question is, how would I draw a straight line through those points, so that it would look the same as
graphic.DrawLine(pen, 0, 0, Control.Width-1, Control.Height-1);
Thanks in advance!
Since you are doing a graph, I have a basic framework for fitting values within a control like this example:
public struct MyPoint
{
public double X { set; get; }
public double Y { set; get; }
public PointF ToPoint()
{
return new PointF((float)X, (float)Y);
}
}
/// <summary>
/// For http://stackoverflow.com/questions/19459519/drawing-a-straight-line-from-points
/// </summary>
public partial class Form1 : Form
{
List<MyPoint> points;
public Form1()
{
InitializeComponent();
// Initialize points to draw
points=new List<MyPoint>(100);
for (int i=0; i<=100; i++)
{
double θ=2*Math.PI*(i/100.0);
double x=(1+θ/Math.PI)*Math.Cos(θ);
double y=(1+θ/Math.PI)*Math.Sin(θ);
points.Add(new MyPoint() { X=x, Y=y });
}
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
// smooth graphics
e.Graphics.SmoothingMode=SmoothingMode.AntiAlias;
// set margins inside the control client area in pixels
var margin=new System.Drawing.Printing.Margins(16, 16, 16, 16);
// set the domain of (x,y) values
var range=new RectangleF(-3, -3, 6, 6);
// scale graphics
ScaleGraphics(e.Graphics, pictureBox1, range, margin);
// convert points to pixels
PointF[] pixels=points.Select((v) => v.ToPoint()).ToArray();
// draw arrow axes
using (var pen=new Pen(Color.Black, 0))
{
pen.EndCap=System.Drawing.Drawing2D.LineCap.ArrowAnchor;
e.Graphics.DrawLine(pen, range.Left, 0.0f, range.Right, 0.0f);
e.Graphics.DrawLine(pen, 0.0f, range.Top, 0.0f, range.Bottom);
}
// draw bounding rectangle (on margin)
using (var pen=new Pen(Color.LightGray, 0))
{
pen.DashStyle=DashStyle.Dash;
e.Graphics.DrawRectangle(pen, Rectangle.Round(range));
}
// draw curve
using (var pen = new Pen(Color.Blue, 0))
{
//e.Graphics.DrawLines(pen, pixels);
e.Graphics.DrawCurve(pen, pixels);
}
}
/// <summary>
/// Scales the Graphics to fit a Control, given a domain of x,y values and side margins in pixels
/// </summary>
/// <param name="g">The Graphics object</param>
/// <param name="control">The Control</param>
/// <param name="domain">The value domain</param>
/// <param name="margin">The margin</param>
void ScaleGraphics(Graphics g, Control control, RectangleF domain, Margins margin)
{
// Find the drawable area in pixels (control-margins)
int W=control.Width-margin.Left-margin.Right;
int H=control.Height-margin.Bottom-margin.Top;
// Ensure drawable area is at least 1 pixel wide
W=Math.Max(1, W);
H=Math.Max(1, H);
// Find the origin (0,0) in pixels
float OX=margin.Left-W*(domain.Left/domain.Width);
float OY=margin.Top+H*(1+domain.Top/domain.Height);
// Find the scale to fit the control
float SX=W/domain.Width;
float SY=H/domain.Height;
// Transform the Graphics
g.TranslateTransform(OX, OY);
g.ScaleTransform(SX, -SY);
}
private void pictureBox1_SizeChanged(object sender, EventArgs e)
{
// redraw on resize
pictureBox1.Refresh();
}
}
I think the problem is because your points are Integers. Precisions are lost.
Trying using
float x = (float)((Values[i] ...
List<System.Drawing.PointF> PointFs;
instead of
int x = (int)((Values[i] ...
List<System.Drawing.Point> Points;
You probably need something like this:
private void DrawLine()
{
int xInitial = 0, yInitial = 0, xFinal = Control.Width - 1, yFinal = Control.Height - 1;
int dx = xFinal - xInitial, dy = yFinal - yInitial, steps, k, xf, yf;
float xIncrement, yIncrement, x = xInitial, y = yInitial;
if (Math.Abs(dx) > Math.Abs(dy))
steps = Math.Abs(dx);
else
steps = Math.Abs(dy);
xIncrement = dx / (float)steps;
yIncrement = dy / (float)steps;
for (k = 0; k < steps; k++)
{
x += xIncrement;
xf = (int)x;
y += yIncrement;
yf = (int)y;
Points.Add(new System.Drawing.Point(xf, yf));
}
graphic.DrawLines(pen, Points.ToArray());
}
For more details see:
Bresenham's line algorithm
If your points already calculated correctly by algorithm, than simple:
int x = (int) Values[i].X;
int y = (int) Values[i].Y;
Should be enough.
here is my solution:
System.Drawing.Pen myPen;
myPen = new System.Drawing.Pen(System.Drawing.Color.Red);
System.Drawing.Graphics formGraphics = this.CreateGraphics();
formGraphics.DrawLine(myPen, 0, 0, 200, 200);
myPen.Dispose();
formGraphics.Dispose();

Visibility of Platonic solids

I'm doing what will draw platonic solids with visibility and constant shading. I have to do it without opengl or directX. I'm solving now the visibility. I could be probably solved by painters alghoritm but I don't know how to implement it in my code. There is how I draw it.
So I'm asking for advice how to implement the painters alghoritm in my code. I allready know something about this alghoritm - teoreticaly. And it will be probably enought just do the step with sorting of faces by the z - coordinate.
For projection I use class projection matrix and I have arrays vertexBuffer and indexBuffer like in OpenGL.
I do all in Visual studio 2010 in C#.
ProjectionMatrix
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace KPG3D
{
/// <summary>
/// Structure of 3D point
/// </summary>
public struct Point3D
{
public float X;
public float Y;
public float Z;
public Point3D(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
public class ProjectionMatrix
{
public float[,] projectionMatrix = new float[4, 4]; //projection matrix
/// <summary>
/// Konstructor of projection matrix
/// </summary>
public ProjectionMatrix()
{
setIdentity();
}
/// <summary>
/// Konstructor of projection matrix
/// </summary>
public ProjectionMatrix(float zNear, float zFar, float viewportWidth, float viewportHeight)
{
setIdentity();
float Q = zFar / (zFar - zNear);
float w = 2 * zNear / viewportWidth;
float h = 2 * zNear / viewportHeight;
projectionMatrix[0, 0] = w;
projectionMatrix[1, 1] = h;
projectionMatrix[2, 2] = Q;
projectionMatrix[3, 2] = 1;
projectionMatrix[2, 3] = -Q * zNear;
}
/// <summary>
/// Konstructor of projection matrix
/// </summary>
public ProjectionMatrix(float d, float z)
{
setIdentity();//set identity matrix
projectionMatrix[0, 0] = d / (d - z);
projectionMatrix[1, 1] = d / (d - z);
projectionMatrix[2, 2] = 0;
}
/// <summary>
/// Set the matrix to identity
/// </summary>
public void setIdentity()
{
projectionMatrix[0, 0] = 1;
projectionMatrix[0, 1] = 0;
projectionMatrix[0, 2] = 0;
projectionMatrix[0, 3] = 0;
projectionMatrix[1, 0] = 0;
projectionMatrix[1, 1] = 1;
projectionMatrix[1, 2] = 0;
projectionMatrix[1, 3] = 0;
projectionMatrix[2, 0] = 0;
projectionMatrix[2, 1] = 0;
projectionMatrix[2, 2] = 1;
projectionMatrix[2, 3] = 0;
projectionMatrix[3, 0] = 0;
projectionMatrix[3, 1] = 0;
projectionMatrix[3, 2] = 0;
projectionMatrix[3, 3] = 1;
}
/// <summary>
/// Aplicate projection on set point
/// </summary>
/// <param name="p">Point want to transformate</param>
/// <returns>Transformated point</returns>
public Point3D multiply(Point3D p)
{
Point3D result;
float tmp = projectionMatrix[3, 0] * p.X + projectionMatrix[3, 1] * p.Y + projectionMatrix[3, 2] * p.Z + projectionMatrix[3, 3] * 1;
result.X = (projectionMatrix[0, 0] * p.X + projectionMatrix[0, 1] * p.Y + projectionMatrix[0, 2] * p.Z + projectionMatrix[0, 3] * 1) / tmp;
result.Y = (projectionMatrix[1, 0] * p.X + projectionMatrix[1, 1] * p.Y + projectionMatrix[1, 2] * p.Z + projectionMatrix[1, 3] * 1) / tmp;
result.Z = (projectionMatrix[2, 0] * p.X + projectionMatrix[2, 1] * p.Y + projectionMatrix[2, 2] * p.Z + projectionMatrix[2, 3] * 1) / tmp;
return result;
}
}
}
Form
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace KPG3D
{
public partial class Form1 : Form
{
private Bitmap canvasBitmap; //bitmap of drawing area
private Graphics g; //access to graffics
private List<Point3D> vertexBuffer = new List<Point3D>(); //list of all vertices
private List<int> indexBuffer = new List<int>();//list of all indices
private Point3D centroid = new Point3D(0, 0, 0);// center of object
private ProjectionMatrix projection = null; //projection matrix
private bool rotation = false;
private int objectID = 0;
public Form1()
{
InitializeComponent();
//create bitmap and set to canvas
canvasBitmap = new Bitmap(canvas.Width, canvas.Height);
canvas.Image = canvasBitmap;
//prepare grafics
g = Graphics.FromImage(canvasBitmap);
g.SmoothingMode = SmoothingMode.AntiAlias;
Matrix origin = new Matrix();
origin.Translate(canvas.Width / 2, canvas.Height / 2);
g.Transform = origin;
}
/// <summary>
/// Reaction on click on start button
/// </summary>
private void buttonStart_Click(object sender, EventArgs e)
{
if (projection == null)
{
projection = new ProjectionMatrix(1, 100, 1, 1);
//createBox();
//createTetrahedron();
createChosenObject();
}
Timer t = new Timer();
t.Tick += new EventHandler(timerDrawing);//set tick method
t.Interval = 30; //every 30 ms
t.Enabled = true;
t.Start();
}
/// <summary>
/// Create tetrahedron
/// </summary>
private void createTetrahedron()
{
Point3D a = new Point3D(-3,-3, 7);//h
Point3D b = new Point3D( 3,-3,13);//c
Point3D c = new Point3D( 3, 3, 7);//a
Point3D d = new Point3D(-3, 3,13);//f
vertexBuffer.Add(a);
vertexBuffer.Add(b);
vertexBuffer.Add(c);
vertexBuffer.Add(d);
//acb
indexBuffer.Add(0);
indexBuffer.Add(2);
indexBuffer.Add(1);
//adc
indexBuffer.Add(0);
indexBuffer.Add(3);
indexBuffer.Add(2);
//cdb
indexBuffer.Add(2);
indexBuffer.Add(3);
indexBuffer.Add(1);
//adb
indexBuffer.Add(0);
indexBuffer.Add(3);
indexBuffer.Add(1);
centroid = new Point3D(0, 0, 10);
}
/// <summary>
/// Create chosen object
/// </summary>
private void createChosenObject() {
switch (objectID) {
case 1:
createTetrahedron();
break;
case 2:
createHexahedron();
break;
case 3:
createOctahedron();
break;
case 4:
createDodecahedron();
break;
case 5:
createIcosahedron();
break;
default:
//do nothing
break;
}
}
/// <summary>
/// Rotate direcion vector by the specified angle
/// </summary>
/// <param name="vector">Direction vector</param>
/// <param name="angle">Angle of rotation</param>
/// <returns></returns>
private Point3D rotateVector(Point3D vector, Point3D centroid, double angle)
{
vector.X -= centroid.X;
vector.Y -= centroid.Y;
vector.Z -= centroid.Z;
Point3D result;
result.X = vector.X * (float)Math.Cos(angle) - vector.Z * (float)Math.Sin(angle);
result.Z = vector.X * (float)Math.Sin(angle) + vector.Z * (float)Math.Cos(angle);
result.Y = vector.Y;
result.X += centroid.X;
result.Y += centroid.Y;
result.Z += centroid.Z;
return result;
}
/// <summary>
/// Reaction on timer
/// </summary>
private void timerDrawing(Object obj, EventArgs ea)
{
//rotation of object
if (rotation == true)
{
for (int i = 0; i < vertexBuffer.Count; i++)
{
vertexBuffer[i] = rotateVector(vertexBuffer[i], centroid, 0.02);
}
}
//drawing of object
draw(vertexBuffer, indexBuffer);
//refresh what is on canvas
canvas.Invalidate();
}
/// <summary>
///Draw point and triangles
/// </summary>
/// <param name="vert"></param>
/// <param name="ind"></param>
private void draw(List<Point3D> vert, List<int> ind)
{
//clear drawing area
g.Clear(Color.Maroon);
//prepare pen and brush
Pen pen = new Pen(Color.Black, 1);
SolidBrush brush = new SolidBrush(Color.Black);
SolidBrush faceBrush = new SolidBrush(Color.FromArgb(75, Color.Green));
//draw edges
for (int i = 0; i < ind.Count / 3; i++)
{
Point3D A = projection.multiply(vert[ind[3 * i + 0]]);
Point3D B = projection.multiply(vert[ind[3 * i + 1]]);
Point3D C = projection.multiply(vert[ind[3 * i + 2]]);
//count to 2D
PointF a = new PointF(A.X * 200, -A.Y * 200);
PointF b = new PointF(B.X * 200, -B.Y * 200);
PointF c = new PointF(C.X * 200, -C.Y * 200);
g.FillPolygon(faceBrush, new PointF[] { a, b, c });
//draw triangle
g.DrawLine(pen, a, b);
g.DrawLine(pen, b, c);
g.DrawLine(pen, c, a);
}
//draw element
for (int i = 0; i < vert.Count; i++)
{
Point3D p = projection.multiply(vert[i]); //projection from 3D to 2D
g.FillRectangle(brush, p.X * 200 - 2, -p.Y * 200 - 2, 4, 4);
}
}
/// <summary>
/// Printscreen to file
/// </summary>
private void buttonPrintScreen_Click(object sender, EventArgs e)
{
canvas.Image.Save("out.png");
}
private void checkBoxRotate_CheckedChanged(object sender, EventArgs e)
{
if (checkBoxRotate.Checked) rotation = true;
else rotation = false;
}
private void objectChooser_SelectedIndexChanged(object sender, EventArgs e)
{
//set ide of chosen object
if(objectChooser.SelectedItem.Equals("Tetrahedron")) objectID = 1;
else if (objectChooser.SelectedItem.Equals("Box")) objectID = 2;
else if (objectChooser.SelectedItem.Equals("Octahedron")) objectID = 3;
else if(objectChooser.SelectedItem.Equals("Dodecahedron")) objectID = 4;
else if (objectChooser.SelectedItem.Equals("Icosahedron")) objectID = 5;
}
}
}
If you have to account for intersecting triangles (see image), I think the easiest solution will involve a z-buffer (they are pretty easy to create and use). If you do not have to render intersecting triangles, your proposed solution of z sorting will be simpler and will work just fine.

Categories