How to draw the spring like shape using c# drawing class
alt text http://img812.imageshack.us/img812/373/spring.jpg
First of all you'd need to think of a formula that would represent the spring. You could draw a circle and as you're going around it, let the X increase a bit. For instance:
for (double i = 0; i < 50; i += 0.01)
{
int x = (int)(Math.Sin(i) * 10 + i * 3);
int y =(int)(Math.Cos(i) * 10 + 50);
}
See the i variable there as time, and the result x and y the coordinates to draw; you'd traverse the path of the spring in small steps.
You could then create a new Bitmap and use the SetPixel method on those coordinates, and in the OnPaint method of your form, draw the bitmap on it.
If you're any good with math (I'm not :P) you might be able to only plot pixels inside the bitmap - the above example doesn't solve the problem of the minimum and maximum values for i.
This is more of a math problem than a C# one. What you want is to derive a Parametric equation for the curve you wish to draw.
With that go and fill an array of Point objects with values for the parametric equation on a certain interval with a certain step (the smaller the step the more the final drawing will look like the actual shape). Then you can use g.DrawLines (MSDN: DrawLines) to draw the actual curve on a surface.
You can edit the width, color and other properties of the line by modifying parameters of the Pen object.
Your actual code would look like this:
void DrawSpring (Graphics g)
{
List<Point> points = new List<Point>();
double step = 0.01;
for(double t = -2; t < 2; t += step)
{
Point p = new Point();
p.X = XPartOfTheEquation(t);
p.Y = YPartOfTheEquation(t);
points.Add(p);
}
g.DrawLines(new Pen(new SolidBrush(Color.Black), 2f), points.ToArray());
}
Related
Currently I have the following code:
AdjustableArrowCap arrow = new AdjustableArrowCap(10, 15, false);
penStateOutline.CustomEndCap = arrow;
And it draws this:
I have tried all day to make the arrow point to the ellipse itself rather than the center of it..
Update (I was wrong about the cap extending the line; it doesn't!)
To let the line and its cap end at the cirlce's outside you need to make the line shorter by the radius of the circle.
There are two approaches:
You can find a new endpoint that sits on the circle by calculating it, either with Pythagoras or by trigonometry. Then replace the endpoint, i.e. the circle's center, when drawing the line or curve by that new point.
Or put the other way round: You need to calculate a point on the circle as the new endpoint of the line.
It requires a little math, unless the line is horizontal or vertical...
This will work well for straight lines but for curves it may cause considerable changes in the shape, depending on how close the points get and how curved the shape is.
This may or may not be a problem.
To avoid it you can replace the curve points by a series of line points that are close enough to look like a curve when drawn. From the list of points we subtract all those that do not lie inside the circle.
This sounds more complicated than it is as there is a nice class called GraphicsPath that will allow you to add a curve and then flatten it. The result is a more or less large number of points. The same class also allows you to determine whether a point lies inside a shape, i.e. our case inside the circle.
To implement the latter approach, here is a routine that transforms a list of curve points to a list of line points that will end close to the circle..:
void FlattenCurveOutside(List<Point> points, float radius)//, int count)
{
using (GraphicsPath gp = new GraphicsPath())
using (GraphicsPath gpc = new GraphicsPath())
{
// firt create a path that looks like our circle:
PointF l = points.Last();
gpc.AddEllipse(l.X - radius, l.Y - radius, radius * 2, radius* 2);
// next one that holds the curve:
gp.AddCurve(points.ToArray());
// now we flatten it to a not too large number of line segments:
Matrix m = new Matrix(); // dummy matrix
gp.Flatten(m, 0.75f); // <== play with this param!!
// now we test the pathpoints from bach to front until we have left the circle:
int k = -1;
for (int i = gp.PathPoints.Length - 1; i >= 0; i--)
{
if ( !gpc.IsVisible(gp.PathPoints[i])) k = i;
if (k > 0) break;
}
// now we know how many pathpoints we want to retain:
points.Clear();
for (int i = 1; i <= k; i++)
points.Add(Point.Round(gp.PathPoints[i]));
}
}
Note that when the last part of the curve is too straight the result may look a little jagged..
Update 2
To implement the former approach here is a function that returns a PointF on a circle of radius r and a line connecting a Point b with the circle center c:
PointF Intersect(Point c, Point a, int rad)
{
float dx = c.X - a.X;
float dy = c.Y - a.Y;
var radians = Math.Atan2(dy, dx);
var angle = 180 - radians * (180 / Math.PI);
float alpha = (float)(angle * Math.PI / 180f);
float ry = (float)(Math.Sin(alpha) * rad );
float rx = (float)(Math.Cos(alpha) * rad );
return new PointF(c.X + rx, c.Y - ry);
}
The result thankfully looks rather similar:
The math can be simplified a little..
To apply it you can get the new endpoint and replace the old one:
PointF I = Intersect(c, b, r);
points2[points2.Count - 1] = Point.Round(I);
Thank you so much to #TaW for helping.
Unfortunately the answer he provided did not match my stupid OCD..
You made me think about the problem from a different perspective and I now have found a solution that I'll share if anyone else needs, note that the solution is not perfect.
public static Point ClosestPointOnCircle(int rad, PointF circle, PointF other)
{
if (other.X == circle.X && other.Y == circle.Y) // dealing with division by 0
other.Y--;
return new Point((int)Math.Floor(circle.X + rad * ((other.X - circle.X) / (Math.Sqrt(Math.Pow(other.X - circle.X, 2) + Math.Pow(other.Y - circle.Y, 2))))), (int)Math.Floor(circle.Y + rad * ((other.Y - circle.Y) / (Math.Sqrt(Math.Pow(other.X - circle.X, 2) + Math.Pow(other.Y - circle.Y, 2))))));
}
What the code does is return a point on the circle that is the closest to the another point, then, I used the curve middle point as the other point to change the end point of the curve.
Then I used the arrow cap as normal and got this:image
Which is good enough for my project.
I use the C# Chart in WinForms to plot a variety of variables in real time using the "line" chart type. That works well for analog values, but it's less than ideal for on/off flags.
I'd like to plot multiple flags as horizontal bars that are filled when the value is '1" and clear when the value is '0'.
Before I start coding a solution from scratch, do you have any suggestion on how I could take advantage of any features of the "chart" object to implement this more effectively?
EDIT: I am playing with the Area type, and it seems to be promising.
EDIT 2: That didn't work, because the area in the Area type always starts at the bottom of the chart, hiding the other rows. I am now trying the Range Column type
There are several ways to tackle this.: StackedBars, AreaChart, Annotations but I think by far the simplest is using a LineChartType.
The first issue is: How to create the gaps? The simplest way is to draw them as lines but with Color.Transparent. So instead of using the flag value as our y-value we use it to set the color..
So we could use a function like this:
void AddFlagLine(Chart chart, int series, int flag, int x)
{
Series s = chart.Series[series];
int px = s.Points.AddXY(x, series);
s.Points[px].Color = s.Color;
if (px > 0) s.Points[px - 1].Color = flag == 1 ? s.Color : Color.Transparent;
}
It takes the index of your Series and uses the flag to determine the color; note that the color of a line segment is controlled by the color of the end point.
So if you want to have the line going out from the new point to have its flag color, you need to set it when adding the next one..
This is simple enough and for lines as thick as 1-10 it works fine. But if you want larger widths things get a bit ugly..:
The rounded caps start to get bigger and bigger until they actually touch, flling the gaps more or less.
Unfortunately there seems to be no way to controls the caps-style of the lines. There are many CustomAttributes including DashStyles but not this one. So we have to resort to owner-drawing. This is rather simple for line charts. Here is an example:
The xxxPaint event looks like this:
private void chart_PostPaint(object sender, ChartPaintEventArgs e)
{
Graphics g = e.ChartGraphics.Graphics;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
for (int si = 0; si < chart.Series.Count; si++ )
{
Series s = chart.Series[si];
for (int pi = 1; pi < s.Points.Count - 1; pi++)
{
DataPoint dp = s.Points[pi];
int y = (int) ay.ValueToPixelPosition(dp.YValues[0]+1); ///*1*
int x0 = (int)ax.ValueToPixelPosition(ax.Minimum);
int x1 = (int)ax.ValueToPixelPosition(s.Points[pi-1].XValue); ///*2*
int x2 = (int)ax.ValueToPixelPosition(dp.XValue);
x1 = Math.Max(x1, x0);
x2 = Math.Max(x2, x0);
using (Pen pen = new Pen(dp.Color, 40) ///*3*
{ StartCap = System.Drawing.Drawing2D.LineCap.Flat,
EndCap = System.Drawing.Drawing2D.LineCap.Flat })
{
g.DrawLine(pen, x1, y, x2, y);
}
}
}
A few notes:
1 : I have decided to move the the series up by one; this is up to you just as using or turning off the y-axis labels or replacing them by custom labels..
2 : Here we use the previous point's x-position!
3 : Note that instead of hard coding a width of 40 pixels you really should decide on a calculated width. This is an example that almost fills up the area:
int width = (int)( ( ay.ValueToPixelPosition(ay.Minimum) -
ay.ValueToPixelPosition(ay.Maximum)) / (chart7.Series.Count + 2));
You can twist is to fill more or less by adding less or more than 2.
I have turned all BorderWidths to 0 so only the drawn lines show.
I got it:
It turned out to actually be pretty easy; I used the Range Column type.
A) Set-up (done once):
plotChart.Series[chanNo].ChartType = SeriesChartType.RangeColumn;
plotChart.Series[chanNo].CustomProperties = "PointWidth=" + noOfFlags;
PointWidth is required to set the relative width of each rectangle so that it fills the entire width of one data point (if too small, there are gaps in the horizontal bar; if too large, there is overlap). noOfFlags is the number of flags shown (in the example shown above, noOfFlags = 4). (By the way the MSDN documentation is wrong: PointWidth is not limited to 2.)
B) Plotting (done for each new data point):
baseLine--;
int barHeight = flagHigh ? 1 : 0;
plotChart.Series[chanNo].Points.AddXY(pointX, baseLine, baseLine + barHeight);
flagHigh is a bool that is equal to the flag being monitored.
baseLine is decremented for each trace. In the example above, baseLine starts at 4, and is decremented down to 0.
Note that for each data point, RangeColumn requires 2 "Y" values: one for the bottom of the rectangle, one for the top; in the code, I set the bottom Y to the bottom of the row that I use for that particular flag, and the top to 1 above the bottom, to give me a height of 1.
I answered a question on how to set up a Chart to look like a regular mathematical graph, i.e. with the axes centered and with nice arrows to the top and to the right..
However I found the built-in AxisArrowStyle.Triangle to be rather big and found no way to make it smaller.
Lines - A line-shaped arrow is used for the relevant axis.
None - No arrow is used for the relevant axis.
SharpTriangle - A sharp triangular arrow is used for the relevant axis.
Triangle - A triangular arrow is used for the relevant axis.
Here is the original look of it:
So how can we fix this?
The Chart's axis.AxisArrowStyle enumeration doesn't let us pick a smaller arrow, only a slimmer one.
So we need to draw it ourselves:
Here is a simple but effective piece of code that achieves just that:
private void chart1_PrePaint(object sender, ChartPaintEventArgs e)
{
if (e.ChartElement.ToString().StartsWith("ChartArea-") )
{
// get the positions of the axes' ends:
ChartArea CA = chart1.ChartAreas[0];
float xx = (float)CA.AxisX.ValueToPixelPosition(CA.AxisX.Maximum);
float xy = (float)CA.AxisY.ValueToPixelPosition(CA.AxisY.Crossing);
float yx = (float)CA.AxisX.ValueToPixelPosition(CA.AxisX.Crossing);
float yy = (float)CA.AxisY.ValueToPixelPosition(CA.AxisY.Maximum);
// a simple arrowhead geometry:
int arrowSize = 18; // size in pixels
Point[] arrowPoints = new Point[3] { new Point(-arrowSize, -arrowSize / 2),
new Point(-arrowSize, arrowSize / 2), Point.Empty };
// draw the two arrows by moving and/or rotating the graphics object:
e.ChartGraphics.Graphics.TranslateTransform(xx + arrowSize, xy);
e.ChartGraphics.Graphics.FillPolygon(Brushes.Black, arrowPoints);
e.ChartGraphics.Graphics.TranslateTransform(yx -xx -arrowSize, yy -xy -arrowSize);
e.ChartGraphics.Graphics.RotateTransform(-90);
e.ChartGraphics.Graphics.FillPolygon(Brushes.Black, arrowPoints);
e.ChartGraphics.Graphics.ResetTransform();
}
}
So i'm trying to write a simple 3d-application (for study only).
I want to do shading using Phong. So i'm reading this article and see
Since this can be slow, there are various ways you can speed it up if you don't mind a little loss of freedom. If you assume that the light source is at the same place as the camera, then you can ignore the V and H vectors altogether. Instead take the X and Y components of the normal vector, multiply by 128 and add 127 (assuming that is that the magnitude of the normal vector is 1).
okay, i can do it, but now, what should I do with this X and Y? I have array of triangles, each has calculated normal. I'm calculating this X and Y, but what should i do with? This code works, but not properly, because in this case light source is in 0,0,0 when i assume that it's in camera point.
Invalid code is here:
public override void Draw(Graphics graphics, VisualizableShape shape)
{
if (!shape.IsVisible)
return;
base.Draw(graphics, shape); // draw edge
int x = (int) (shape.Normal.X*128 + 127);
int y = (int) (shape.Normal.Y*128 + 127);
int z = x + y;
var color = Color.FromArgb(255, (Pen.Color.R*z)/511, ((Pen.Color.G*z)/511), (Pen.Color.B*z)/511);
graphics.FillPolygon(new SolidBrush(color), shape.Points.CastToPointF());
}
just dunno what should I do in this case...
In my C# WinForms application I have a picturebox that hosts 2 curves (Resulted from a voltage/current measurement). The X axis is voltage and Y axis is current. The voltage axis is ranged from -5 to 5 but the current axis is a much smaller scale ranged from -10 uA to 10 uA. The task is to see if the second curve is within 10% of the first curve.
For visual inspection I am trying to draw an envelope around the first curve (Blue one). The curve is just a PointF array. At the moment since I have no idea how to draw a correct envelope around the blue curve, I just draw two other curves that are result of X points of the actual curve added and subtracted by 10% of the original curve. Of course this is a bad approach, but atleast for the section of the curve that is noticably vertical, it works. But as soon as the curve is on its non vertical section, this trick does not work anymore, as you can see in the picture below:
Here is the code that I am using to draw the envelope:
public Bitmap DrawEnvelope(double[,] pinData, float vLimit, float iLimit)
{
g = Graphics.FromImage(box);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
PointF[] u = new PointF[pinData.GetLength(0)]; //Up line
PointF[] d = new PointF[pinData.GetLength(0)]; //Down Line
List<PointF> joinedCurves = new List<PointF>();
float posX = xMaxValue * (vLimit / 100);
float minX = posX * -1;
for (int i = 0; i < pinData.GetLength(0); i++)
{
u[i] = new PointF(400 * (1 + (((float)pinData[i, 0]) + minX) / (xMaxValue + vExpand)), 400 * (1 - ((float)pinData[i, 1] * GetInvers((yMaxValue + iExpand)))));
}
for (int i = 0; i < pinData.GetLength(0); i++)
{
d[i] = new PointF(400 * (1 + (((float)pinData[i, 0]) + posX) / (xMaxValue + vExpand)), 400 * (1 - ((float)pinData[i, 1] * GetInvers((yMaxValue + iExpand)))));
}
Pen pengraph = new Pen(Color.FromArgb(50, 0 ,0 ,200), 1F);
pengraph.Alignment = PenAlignment.Center;
joinedCurves.AddRange(u);
joinedCurves.AddRange(d.Reverse());
PointF[] fillPoints = joinedCurves.ToArray();
SolidBrush fillBrush = new SolidBrush(Color.FromArgb(40, 0, 0, 250));
FillMode newFillMode = FillMode.Alternate;
g.FillClosedCurve(fillBrush, fillPoints, newFillMode, 0);
g.Dispose();
return box;
}
The green circles are added by myself, and they indicate the region that the second curve (Red one) is potentially has a difference bigger than 10% from the orginal curve.
Would be nice if someone put me in the right way, what should I look to to achive a nice envelope around original curve?
UPDATE
Because I am so noob I cant find a way to implement the answers given to this question until now, So put a bounty to see if somone can kindly show me atleast a coding approach to this problem.
You could try finding the gradient between each pair of points and calculating two points either side that are on the orthogonal that passes through the midpoint.
You would then have two more lines defined as a set of points that you could use to draw the envelope.
Your best bet is to iterate your point array and to calculate a perpendicular vector to two consecutive points each time (see Calculating a 2D Vector's Cross Product for implementation clues). Project in either direction along these perpendicular vectors to generate the two point arrays of your envelope.
This function generates them roughly using segment midpoints (as long as the point count is high and your offset is not too small it should look ok when plotted):
private void GetEnvelope(PointF[] curve, out PointF[] left, out PointF[] right, float offset)
{
left = new PointF[curve.Length - 1];
right = new PointF[curve.Length - 1];
for (int i = 1; i < curve.Length; i++)
{
PointF normal = new PointF(curve[i].Y - curve[i - 1].Y, curve[i - 1].X - curve[i].X);
float length = (float)Math.Sqrt(normal.X * normal.X + normal.Y * normal.Y);
normal.X /= length;
normal.Y /= length;
PointF midpoint = new PointF((curve[i - 1].X + curve[i].X) / 2F, (curve[i - 1].Y + curve[i].Y) / 2F);
left[i - 1] = new PointF(midpoint.X - (normal.X * offset), midpoint.Y - (normal.Y * offset));
right[i - 1] = new PointF(midpoint.X + (normal.X * offset), midpoint.Y + (normal.Y * offset));
}
}
It all depends on the way you want the envelop to be sized.
You could calculate/guestimate the slope of the curve in each point by calculating the slope to the next point and the slope to the previous point, average these and then calculate a perpendicular vector to the slope.
Add this vector to the point of the curve; this gives you the right-hand edge of the envelop.
Subtract this vector from the point of the curve; this gives you the left-hand edge of the envelop.
This method will fail if the points are too far apart or very sudden changes in the points appear.
This is probably a dumb suggestion. Perhaps instead of drawing the envelope yourself, maybe you could let winforms do it for you. Try drawing the envelope as a line with a pen that has a larger width. Perhaps it might work.
If you look at this msdn example on varying the pen width, you might see what I mean.
http://msdn.microsoft.com/en-us/library/3bssbs7z.aspx
2 (probably incorrect) possibilities.
Do what you did originally to get the pale blue wide area, but also do it in the vertical direction (not just the horizontal)
Do what Dan suggested with a REALLY thick line (in pale blue) then draw it again, then draw the original (thin) line on top of it.