Tooltip in PictureBox FillPie Coordinates C# - c#

I'm drawing a Circle made of 360 FillPie. Each FillPie color is taken from a List. I want to return a string that says at which degree is the mouse and how much is the value of the list to put it on a tooltip.
List<int> datiDisco = new List<int>();
public void Paint (Graphics grafica)
{
try
{
for (int i = 0; i < datiDisco.Count; i++)
{
Brush penna = new SolidBrush(Color.FromArgb(255, ScalaGrigi(valori[i]), ScalaGrigi(valori[i]), ScalaGrigi(valori[i])));
grafica.FillPie(penna, 0, 0, 400, 400, i, 1.0f);
}
}
catch
{
}
}
until here the code is working and i managed to draw the circle with the correct color.Now i can't figure out how i can take the coordinate of each fillpie that i have drawn. Can someone help me?

Figuring out which pie segment the mouse cursor lies in is a simple application of trigonometry, specifically an application of the inverse tangent (aka arctangent or atan).
As a quick reminder for those who've encountered this before, or as a lesson for those who haven't, let's look quickly at the tangent function. Trigonometry deals with the geometry of right triangles, and by definition a right triangle has two sides and a hypotenuse. The hypotenuse is a special name for the side of the triangle opposite the right (90° or π/2) angle. The other two sides are helpfully just called sides.
The tangent function's value is the ratio of the side opposite an angle to the side adjacent to that angle. The arctangent is the angle whose tangent is equal to that ratio. Because of the symmetry of the function we need to calculate the angle, and then add or subtract an offset depending on the quadrant to extract the 'real' angle. In diagram form this looks like:
The tangent function has discontinuities at several points, namely when the adjacent side's length is 0 (90° and 270°), so we'll have to treat those points specially.
OK, enough math, now on to the practical application.
For this demo, create a new C# WinForms project, and on the default Form1 add a PictureBox.
First, since I don't have your color generation function, I use the following list of values and helper function:
List<int> values = Enumerable.Range(0, 360).ToList();
int Rescale(int x) => (int)(((double)x / 360.0) * 255.0);
In the constructor hook up a couple events, and set some properties:
public Form1()
{
InitializeComponent();
this.pictureBox1.BorderStyle = BorderStyle.Fixed3D;
this.pictureBox1.Size = new Size(50, 50);
this.Size = new Size(450, 450);
this.DoubleBuffered = true;
this.Paint += Form1_Paint;
this.MouseMove += Form1_MouseMove;
}
To paint the circle I use a slightly modified version of your OnPaint handler:
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(Color.Black);
for (int i = 0; i < values.Count; i++)
{
Brush b = new SolidBrush(Color.FromArgb(255, Rescale(values[i]), 0, 0));
e.Graphics.FillPie(b, 0, 0, 400, 400, (float)i, 1.0f);
}
}
In the MouseMove event is where we do most of the heavy lifting:
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
this.pictureBox1.Location = new Point(e.X + 5, e.Y - 5);
int segment = (int)GetAngle(new Rectangle(0, 0, 400, 400), e.Location);
this.pictureBox1.BackColor = Color.FromArgb(255, Rescale(segment), 0, 0);
}
You may notice that since there are 360 wedges are in increments of a degree, I just truncated the angle. If you need more precision, or you decide to use segments greater than 1 degree, then you could use various rounding algorithms to round the angle to the nearest section of the pie.
At last, we're ready to implement the GetAngle function. First we calculate the center of the circle, because everything is relative to that.
int cx = (rect.Width + rect.X) / 2;
int cy = (rect.Height + rect.Y) / 2;
Next calculate the difference between the mouse's position and the center of the rectangle. (I've inverted the y coordinate to line up with 'standard' Cartesian coordinates, to make things easier, and match the coordinates you'd see in a math textbook.)
float x = pTo.X - cx;
float y = (cy - pTo.Y);
Next check for the arctangent's undefined points (and a couple of shortcuts we can take):
if ((int)x == 0)
{
if (y > 0) return 270;
else return 90;
}
else if ((int)y == 0)
{
if (x > 0) return 0;
else return 180;
}
Calculate the internal angle:
float ccwAngle = (float)Math.Atan(Math.Abs(y) / Math.Abs(x));
And map that angle to the appropriate quadrant:
if (x > 0 && y > 0)
{
}
else if (x < 0 && y > 0)
{
ccwAngle = (float)Math.PI - ccwAngle;
}
else if (x < 0 && y < 0)
{
ccwAngle = ccwAngle + (float)Math.PI;
}
else if (x > 0 && y < 0)
{
ccwAngle *= -1f;
}
Convert the angle from degrees to radians and normalize (make sure it's between 0° and 360°)
ccwAngle *= (float)(180 / Math.PI);
while (ccwAngle > 360) ccwAngle -= 360;
while (ccwAngle < 0) ccwAngle += 360;
Finally convert the counter-clockwise angle we needed to do the math into the clockwise angle that GDI uses, and return the value:
return 360f - ccwAngle;
All that together produces the final result:
(The code above is also available as a complete example in this gist)

Related

GDI+ curve "overflowing"

I'm currently using GDI+ to draw a line graph, and using Graphics.DrawCurve to smooth out the line. The problem is that the curve doesn't always match the points I feed it, and that makes the curve grow out of the graph frame in some points, as seen below(red is Graphics.DrawLines, green is Graphics.DrawCurve).
How would I go about solving this?
The simplest solution is to set a tension:
The green curve is drawn with the default tension, the blue one set a tension of 0.1f:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.DrawLines(Pens.Red, points.ToArray());
e.Graphics.DrawCurve(Pens.Green, points.ToArray());
e.Graphics.DrawCurve(Pens.Blue, points.ToArray(), 0.1f);
}
You will need to test what is the best compromise, 0.2f is still ok, 0.3f is already overdrawing quite a bit..
For a really good solution you will need to use DrawBeziers. This will let you draw curves that can go through the points without any overdrawing and with full control of the radius of the curves; but to to so you will need to 'find', i.e. calculate good control points, which is anything but trivial..:
This result is by no means perfect but already complicated enough.. I have displayed the curve points and their respective control points in the same color. For each point there is an incoming and an outgoing control point. For a smooth curve they need to have the same tangents/gradients in their curve points.
I use a few helper functions to calculate a few things about the segments:
A list of gradients
A list of signs of the gradients
A list of segment lengths
Lists of horizontal and of vertical gaps between points
The main function calculates the array of bezier points, that is the curve points and between each pair the previous left and the next right control points.
In the Paint event it is used like this:
List<PointF> bezz = getBezz(points);
using (Pen pen = new Pen(Color.Black, 2f))
e.Graphics.DrawBeziers(pen, bezz.ToArray());
Here are the functions I used:
List<float> getGradients(List<PointF> p)
{
List<float> grads = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
if (dx == 0) grads.Add(dy == 0 ? 0 : dy > 0 ?
float.PositiveInfinity : float.NegativeInfinity);
else grads.Add(dy / dx);
}
return grads;
}
List<float> getLengths(List<PointF> p)
{
List<float> lengs = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
lengs.Add((float)Math.Sqrt(dy * dy + dx * dx));
}
return lengs;
}
List<float> getGaps(List<PointF> p, bool horizontal)
{
List<float> gaps = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
gaps.Add(horizontal ? dx : dy);
}
return gaps;
}
List<int> getSigns(List<float> g)
{
return g.Select(x => x > 0 ? 1 : x == 0 ? 0 : -1).ToList();
}
And finally the main function; here I make a distinction: Extreme points ( minima & maxima) should have their control points on the same height as the points themselves. This will prevent vertical overflowing. They are easy to find: The signs of their gradients will always altenate.
Other points need to have the same gradient for incoming and outcoming control points. I use the average between the segments' gradients. (Maybe a weighed average would be better..) And I weigh their distance according to the segment lengths..
List<PointF> getBezz(List<PointF> points)
{
List<PointF> bezz = new List<PointF>();
int pMax = points.Count;
List<float> hGaps = getGaps(points, true);
List<float> vGaps = getGaps(points, false);
List<float> grads = getGradients(points);
List<float> lengs = getLengths(points);
List<int> signs = getSigns(grads);
PointF[] bezzA = new PointF[pMax * 3 - 2];
// curve points
for (int i = 0; i < pMax; i++) bezzA[i * 3] = points[i];
// left control points
for (int i = 1; i < pMax; i++)
{
float x = points[i].X - hGaps[i - 1] / 2f;
float y = points[i].Y;
if (i < pMax - 1 && signs[i - 1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y - hGaps[i-1] / 2f * m * vGaps[i-1] / lengs[i-1];
}
bezzA[i * 3 - 1] = new PointF(x, y);
}
// right control points
for (int i = 0; i < pMax - 1; i++)
{
float x = points[i].X + hGaps[i] / 2f;
float y = points[i].Y;
if (i > 0 && signs[i-1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y + hGaps[i] / 2f * m * vGaps[i] / lengs[i];
}
bezzA[i * 3 + 1] = new PointF(x, y);
}
return bezzA.ToList();
}
Note that I didn't code for the case of points with the same x-coordinate. So this is ok for 'functional graphs' but not for, say figures, like e.g. stars..
Maybe you just want to look at the "overshooting the bounds" problem as not a problem with the overshoot, but with the bounds. In which case, you can determine the actual bounds of a curve using the System.Drawing.Drawing2D.GraphicsPath object:
GraphicsPath gp = new GraphicsPath();
gp.AddCurve(listOfPoints);
RectangleF bounds = gp.GetBounds();
You can draw that GraphicsPath directly:
graphics.DrawPath(Pens.Black, gp);
As far as solving the bounds problem, the line necessarily overshoots the vertex on some axis. It's easier to see this fact when the lines are aligned to the bounds.
Given these points:
In order for them to be curved, they must exceed their bounds in some way:
If you never want to exceed their vertical bounds, you could simply ensure that the bezier handles have the same Y value as the vertex, but they will overshoot on the X:
Or vice-versa:
You could deliberately undershoot just enough to avoid the way curves can overshoot. This can be done by swapping the bezier handles, which would maybe be at the line-centers, with the vertices:

Move PictureBox using Timer

My goal is to move a picturebox back and forth. My issue is how to do this.
I have written the following:
int x = enemy.Location.X;
int y = enemy.Location.Y;
enemy.Location = new Point(x+-1, y);
This moves the picturebox off-screen, left. After moving left, I'd like it to move right, so that it moves back and forth - in a continuous loop.
The noob that I am, I tried:
if (x < 40)
enemy.Location = new Point(x - -100, y);
else if (x > 400)
enemy.Location = new Point(x - 5, y);
This proves unsuccessful - the box doesn't seem to move on reaching pixel 40.
Is there a simple solution that you can prod me towards, or have I dug an early grave for myself?!
I should specify: I am writing in C# per college assignment requirements.
Cheers.
When moving left, when the x location reaches 0, change direction and move right.
When moving right, you need to use the width of the screen minus the width of your picturebox.
System.Windows.SystemParameters.PrimaryScreenWidth
edit:
Or better yet, use the width of your form minus the width of the picturebox. Then it will still work if its not maximized.
Setup a variable that toggles between negative and positive values to make it go left and right. You toggle the direction by multiplying by -1. Then you simply add that variable to the current X value like this:
private int direction = -1; // this can be values other than 1 to make it jump farther each move
private void timer1_Tick(object sender, EventArgs e)
{
int x = enemy.Location.X + direction;
if (x <= 0)
{
direction = -1 * direction;
x = 0;
}
else if (x >= this.ClientRectangle.Width - enemy.Width)
{
direction = -1 * direction;
x = this.ClientRectangle.Width - enemy.Width;
}
enemy.Location = new Point(x, enemy.Location.Y);
}

WinForm Triangle animation

I am trying to make a triangle move back and forth over an arc, the triangle shoud rotate while moving.
I have made a picture to explain it better:
https://app.box.com/s/mt9p66zlmtkkgkdvtb5h
The math looks right to me, can anyone tell me what I am doing wrong?
public partial class Form1 : Form
{
bool turn = false;
double angle = 0;
public Form1()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Brush solidBlackBrush = new SolidBrush(Color.Black); //En solid svart brush som brukes flere steder
Pen solidBackPen = new Pen(solidBlackBrush);//En solid svart pen som brukes flere steder
//Trekant = Norwegian for Triangle, Trekant is a class that draws a polygon shaped as a Triangle.
Trekant tre = new Trekant();
e.Graphics.DrawArc(solidBackPen, new Rectangle(new Point(50,50), new Size(100,100)) , 180, 180);
//X = a + r*Cos(angle) | Y = b + r*Sin(angle)
double x = (50+(100/2)) + (100/2) * Math.Cos(Trekant.DegreeToRadian(angle));
double y = (50+(100/2)) - (100/2) * Math.Sin(Trekant.DegreeToRadian(angle));
e.Graphics.TranslateTransform((float)x - 15, (float)y - 40);//Flytter 0 slik at pistolen havner på rett sted
e.Graphics.RotateTransform((float)-Trekant.RadianToDegree(Trekant.DegreeToRadian(angle-90)));
tre.Draw(e.Graphics);
}
private void timer1_Tick(object sender, EventArgs e)
{
if (angle == 0)
{
turn = false;
}
if (angle == 180)
{
turn = true;
}
if (turn)
{
angle -= 10;
}
if (!turn)
{
angle += 10;
}
this.Invalidate();
}
}
Without going into coding let's first set up the math..
Let say the half ellipse in the picture has a width of 2w and a height of h. And lets assume you want the movement to happen in n steps.
Then at each step s the rotation angle is s * 180f/n. The rotation point's x stays at w plus whatever offset ox the ellipse has, but will have to move its y vertically from offset oy, first by (w-h) * 2f / n down on each step and then up again by the same amounts..
The Drawing itself moves accordingly.
So you have a TranslateTransform for the rotation point, the RotateTransform, then another TranslateTransform to place the image, then the DrawImage and finally a ResetTransform.
I hope that helps. If that doesn't work, please update the question and we'll can get it right, I'm sure..

Calculating the Area of a Closed Polygon on a Plane

I'm attempting to calculate the area of a polygon that lies on a plane (a collection co-planar points forming a non-intersecting closed shape), and I know a method that can calculate the area of an irregular (or any) polygon in two dimensions - but not three. My solution is to rotate the plane so that it's normal is 0 in the z direction (so I can treat it like it's 2D) and then run the 2D area function.
The problem is I have NO idea how to actually determine the rotation axes and amounts to flatten a plane on it's Z-axis. I do my rotation through the easiest method I could find for 3 dimensional rotation: Rotation Matrices. So, given that I'm trying to use rotation matrices to do my rotation, how do I figure out the angles to rotate my plane by to be oriented in the same direction as another vector? I don't actually know much calculus or Euclidean geometry, so whichever solution requires me to teach myself the least of both is the ideal solution. Is there a better way?
Here's my attempt below, which doesn't even come close to getting the plane flat on the Z axis. This is an instance method of my "Surface" class, which is a derivative of my "Plane" class, and has an array of co-planar points (IntersectPoints) forming a closed polygon.
public virtual double GetArea()
{
Vector zUnit = new Vector(0, 0, 1); //vector perprendicualr to z
Vector nUnit = _normal.AsUnitVector();
Surface tempSurface = null;
double result = 0;
if (nUnit != zUnit && zUnit.Dot(nUnit) != 0) //0 = perprendicular to z
{
tempSurface = (Surface)Clone();
double xAxisAngle = Vector.GetAxisAngle(nUnit, zUnit, Physics.Formulae.Axes.X);
double yAxisAngle = Vector.GetAxisAngle(nUnit, zUnit, Physics.Formulae.Axes.Y);
double rotationAngle = Vector.GetAxisAngle(nUnit, zUnit, Physics.Formulae.Axes.Z);
tempSurface.Rotate(xAxisAngle, yAxisAngle, rotationAngle); //rotating plane so that it is flat on the Z axis
}
else
{
tempSurface = this;
}
for (int x = 0; x < tempSurface.IntersectPoints.Count; x++) //doing a cross sum of each point
{
Point curPoint = tempSurface.IntersectPoints[x];
Point nextPoint;
if (x == tempSurface.IntersectPoints.Count - 1)
{
nextPoint = tempSurface.IntersectPoints[0];
}
else
{
nextPoint = tempSurface.IntersectPoints[x + 1];
}
double cross1 = curPoint.X * nextPoint.Y;
double cross2 = curPoint.Y * nextPoint.X;
result += (cross1 - cross2); //add the cross sum of each set of points to the result
}
return Math.Abs(result / 2); //divide cross sum by 2 and take its absolute value to get the area.
}
And here are my core rotation and get axis angle methods:
private Vector Rotate(double degrees, int axis)
{
if (degrees <= 0) return this;
if (axis < 0 || axis > 2) return this;
degrees = degrees * (Math.PI / 180); //convert to radians
double sin = Math.Sin(degrees);
double cos = Math.Cos(degrees);
double[][] matrix = new double[3][];
//normalizing really small numbers to actually be zero
if (Math.Abs(sin) < 0.00000001)
{
sin = 0;
}
if (Math.Abs(cos) < 0.0000001)
{
cos = 0;
}
//getting our rotation matrix
switch (axis)
{
case 0: //x axis
matrix = new double[][]
{
new double[] {1, 0, 0},
new double[] {0, cos, sin * -1},
new double[] {0, sin, cos}
};
break;
case 1: //y axis
matrix = new double[][]
{
new double[] {cos, 0, sin},
new double[] {0, 1, 0},
new double[] {sin * -1, 0, cos}
};
break;
case 2: //z axis
matrix = new double[][]
{
new double[] {cos, sin * -1, 0},
new double[] {sin, cos, 0},
new double[] {0, 0, 1}
};
break;
default:
return this;
}
return Physics.Formulae.Matrix.MatrixByVector(this, matrix);
}
public static double GetAxisAngle(Point a, Point b, Axes axis, bool inDegrees = true)
{ //pretty sure this doesnt actually work
double distance = GetDistance(a, b);
double difference;
switch (axis)
{
case Axes.X:
difference = b.X - a.X;
break;
case Axes.Y:
difference = b.Y - a.Y;
break;
case Axes.Z :
difference = b.Z - a.Z;
break;
default:
difference = 0;
break;
}
double result = Math.Acos(difference / distance);
if (inDegrees == true)
{
return result * 57.2957; //57.2957 degrees = 1 radian
}
else
{
return result;
}
}
A robust way to do this is to do a sum of the cross-products of the vertices of each edge. If your vertices are co-planar, this will produce a normal to the plane, whose length is 2 times the area of the closed polygon.
Note that this method is very similar to the 2D method linked in your question, which actually calculates a 2D equivalent of the 3D cross-product, summed for all edges, then divides by 2.
Vector normal = points[count-1].cross(points[0]);
for(int i=1; i<count; ++i) {
normal += points[i-1].cross(points[i]);
}
double area = normal.length() * 0.5;
Advantages of this method:
If your vertices are only approximately planar, it still gives the right answer
It doesn't depend on the angle of the plane.
In fact you don't need to deal with the angle at all.
If you want to know the plane orientation, you've got the normal already.
One possible difficulty: if your polygon is very small, and a long way away from the origin, you can get floating point precision problems. If that case is likely to arise, you should first translate all of your vertices so that one is at the origin, like so:
Vector normal(0,0,0);
Vector origin = points[count-1];
for(int i=1; i<count-1; ++i) {
normal += (points[i-1]-origin).cross(points[i]-origin);
}
double area = normal.length() * 0.5;
You need not to rotate the plane (or all points). Just calculate an area of polygon projection to Z-plane (if it is not perpendicular to polygon plane), for example, with you GetArea function, and divide result by cosinus of Poly-plane - Z-plane angle - it is equal to scalar product of zUnit and nUnit (I suggest that nUnit is normal vector to polygon plane)
TrueArea = GetArea() / zUnit.Dot(nUnit)

Resize a box with the mouse while maintaining the aspect ratio?

I'm trying to create a resizable image overlay (for cropping purposes). It seems pretty easy to resize the overlay if I ignore the aspect ratio, but I can't figure out how to perform a constrained resize that respects the AR. I figure that I obviously can't obey the overlay's "grip" positions (or even borders) unless I force the mouse to follow it, but that seems unnatural, so I'll just have to rely on the mouse gesture (which I don't mind doing).
I can also easily resize the overlay and then force it into the proper dimensions afterwards (like every other question about this topic on this site is about), but it's not very intuitive when using a mouse.
This is sort of what I'm going for:
http://deepliquid.com/projects/Jcrop/demos.php?demo=live_crop
I've written an application like this before but it was browser-based so I used a javascript library. This is a desktop application and I haven't found a suitable library for this.
I've left a lot of details out of this code snippet and simplified some conditions with booleans.
private void pbImage_Paint(object sender, PaintEventArgs e)
{
//Overlay
e.Graphics.FillRectangle(brushRect, overlayRect);
// Grips
e.Graphics.FillRectangle(gripRect, leftTopGrip);
e.Graphics.FillRectangle(gripRect, rightTopGrip);
e.Graphics.FillRectangle(gripRect, leftBottomGrip);
e.Graphics.FillRectangle(gripRect, rightBottomGrip);
AdjustGrips();
base.OnPaint(e);
}
public void AdjustGrips()
{
// The next section only causes the grips to partly obey
// the AR - the rest of the overlay ignores it
if (overlayRect.Height * arWidth <= overlayRect.Width)
overlayRect.Width = overlayRect.Height * arWidth;
else if (overlayRect.Width * arHeight <= overlayRect.Height)
overlayRect.Height = overlayRect.Width * arHeight;
leftTopGrip.X = overlayRect.Left;
leftTopGrip.Y = overlayRect.Top;
rightTopGrip.X = overlayRect.Right - rightTopGrip.Width;
rightTopGrip.Y = overlayRect.Top;
leftBottomGrip.Y = overlayRect.Bottom - leftBottomGrip.Height;
leftBottomGrip.X = overlayRect.Left;
rightBottomGrip.X = overlayRect.Right - rightBottomGrip.Width;
rightBottomGrip.Y = overlayRect.Bottom - rightBottomGrip.Height;
}
private void pbImage_MouseMove(object sender, MouseEventArgs e)
{
Point pt = new Point(e.X, e.Y);
// Details elided
if (e.Button == MouseButtons.Left && mouseinGrip)
{
if (bottomRightIsGripped)
{
newOverlayRect.X = overlayRect.X;
newOverlayRect.Y = overlayRect.Y;
newOverlayRect.Width = pt.X - newOverlayRect.Left;
newOverlayRect.Height = pt.Y - newOverlayRect.Top;
if (newOverlayRect.X > newOverlayRect.Right)
{
newOverlayRect.Offset(-width, 0);
if (newOverlayRect.X < 0)
newOverlayRect.X = 0;
}
if (newOverlayRect.Y > newOverlayRect.Bottom)
{
newOverlayRect.Offset(0, -height);
if (newOverlayRect.Y < 0)
newOverlayRect.Y = 0;
}
pbImage.Invalidate();
oldOverlayRect = overlayRect = newOverlayRect;
Cursor = Cursors.SizeNWSE;
}
// Code for other grips elided
}
AdjustGrips();
pbImage.Update();
base.OnMouseMove(e);
}
// Mouse up and down elided
You have complete control over the new size for the overlay as it drags.
The example link that you've given, is simply selecting a starting point based on the click down, then selecting Max(Abs(pt.x - start.x), Abs(pt.y - start.y)), and basing the crop square off of that.
To use a non square ratio, normalize the distances first.
// given known data
//
// Point start;
// The starting location of the mouse down for the drag,
// or the top left / bottom right of the crop based on if the mouse is
// left/above the starting point
//
// Size ratio;
// The ratio of the result crop
//
// pt = (20)x(-20)
// start = (0),(0)
// ratio = (1)x(2)
var dist = new Point(pt.X - start.X, pt.Y - start.Y);
// "normalize" the vector from the ratio
// normalized vector is the distances with respect to the ratio
// ratio is (1)x(2). A (20)x(-20) is normalized as (20),(-10)
var normalized = new Point(dist.X / ratio.Width, dist.Y / ratio.Height);
// In our (20),(-10) example, we choose the ratio's height 20 as the larger normal.
// we will base our new size on the height
var largestNormal = (Math.Abs(normalized.X) > Math.Abs(normalized.Y)
? Math.Abs(normalized.X) : Math.Abs(normalized.Y);
// The calcedX will be 20, calcedY will be 40
var calcedOffset = (largestNormal * ratio.Width, largestNormal * ratio.Height);
// reflect the calculation back to the correct quarter
// final size is (20)x(-40)
if (distX < 0) calcedOffset.X *= -1;
if (distY < 0) calcedOffset.Y *= -1;
var newPt = new Point(start.X + calcedOffset.X, start.Y + calcedOffset.Y);
Notice that one of the lengths can grow greater than the mouse location, but it will never be less. This will have the effect of the mouse traveling along the edge of the new crop box, and the box maintaining ratio.
I've figured out what was causing the original problems in my code. Unlike a static image resize, the aspect ratio code depends on which grip you're "holding", so putting it in a common location for all cases (eg. when the grip positions are set) will not work. You can easily calculate the size of the what the rect should be on the next update, but the position should be set depending on which grip is being held.
If, for example, you're resizing by holding the top left grip, then the bottom and right sides of the cropping rectangle should remain stationary. If you leave the code the same, then the rectangle resizes correctly, but it moves around the canvas and/or the grips go out of sync with the corners of the rect. There is probably a better way to do this but here's some crude code that works. I've only included code for the bottom right and top left grips to illustrate the differences. Extraneous things like setting the mouse pointer and error checking omitted.
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
Point mousePosition = new Point(e.X, e.Y);
if (e.Button == MouseButtons.Left)
{
// This resizeMode, moveMode and other booleans
// are set in the MouseUp event
if (resizeBottomLeft)
{
// Top and Right should remain static!
newCropRect.X = mousePosition.X;
newCropRect.Y = currentCropRect.Y;
newCropRect.Width = currentCropRect.Right - mousePosition.X;
newCropRect.Height = mousePosition.Y - newCropRect.Top;
if (newCropRect.X > newCropRect.Right)
{
newCropRect.Offset(cropBoxWidth, 0);
if (newCropRect.Right > ClientRectangle.Width)
newCropRect.Width = ClientRectangle.Width - newCropRect.X;
}
if (newCropRect.Y > newCropRect.Bottom)
{
newCropRect.Offset(0, -cropBoxHeight);
if (newCropRect.Y < 0)
newCropRect.Y = 0;
}
// Aspect Ratio + Positioning
if (newCropRect.Width > newCropRect.Height)
{
newCropRect.Height = (int)(newCropRect.Width / ASPECT_RATIO);
}
else
{
int newWidth = (int)(newCropRect.Height * ASPECT_RATIO);
newCropRect.X = newCropRect.Right - newWidth;
newCropRect.Width = newWidth;
}
}
else if (resizeTopRight)
{
// Bottom and Left should remain static!
newCropRect.X = oldCropRect.X;
newCropRect.Y = mousePosition.Y;
newCropRect.Width = mousePosition.X - newCropRect.Left;
newCropRect.Height = oldCropRect.Bottom - mousePosition.Y;
if (newCropRect.X > newCropRect.Right)
{
newCropRect.Offset(-cropBoxWidth, 0);
if (newCropRect.X < 0)
newCropRect.X = 0;
}
if (newCropRect.Y > newCropRect.Bottom)
{
newCropRect.Offset(0, cropBoxHeight);
if (newCropRect.Bottom > ClientRectangle.Height)
newCropRect.Y = ClientRectangle.Height - newCropRect.Height;
}
// Aspect Ratio + Positioning
if (newCropRect.Width > newCropRect.Height)
{
int newHeight = (int)(newCropRect.Width / ASPECT_RATIO);
newCropRect.Y = newCropRect.Bottom - newHeight;
newCropRect.Height = newHeight;
}
else
{
int newWidth = (int)(newCropRect.Height * ASPECT_RATIO);
newCropRect.Width = newWidth;
}
}
else if (moveMode) //Moving the rectangle
{
newMousePosition = mousePosition;
int dx = newMousePosition.X - oldMousePosition.X;
int dy = newMousePosition.Y - oldMousePosition.Y;
currentCropRect.Offset(dx, dy);
newCropRect = currentCropRect;
oldMousePosition = newMousePosition;
}
if (resizeMode || moveMode)
{
oldCropRect = currentCropRect = newCropRect;
// Set the new position of the grips
AdjustGrips();
pictureBox1.Invalidate();
pictureBox1.Update();
}
}
}

Categories