Rotating a polygon distorts the original shape - c#

When my polygon initially gets drawn after coming in contact with a 'Tag', it's drawn fine with the appropriate size and angles. However, when I rotate it, it gets distorted for eg. a right-angled triangle becomes an acute-like triangle.
So I have a feeling something is wrong with the RotateTransform bit I'm doing but I can't figure out what.
Here is my code for initializing values and rotating a polygon:
public Tangible(Point topLeft, Point topRight, Point bottomLeft, Point bottomRight, Point centerOfTag, BlobPair taglocation, String name, Double tagAngleOffset, String colour, String shapeType)
{
TopLeft = topLeft;
TopRight = topRight;
BottomLeft = bottomLeft;
BottomRight = bottomRight;
TagLocation = taglocation;
Name = name;
CenterOfTag = centerOfTag;
TagAngleOffset = tagAngleOffset;
Colour = colour;
ShapeType = shapeType;
}
public void draw (Canvas tagCanvas)
{
shapeOutline = new Polygon();
//some other bits and pieces of code here
}
public void update(Canvas tagCanvas)
{
Double width = BottomRight.X - BottomLeft.X;
Double height = BottomLeft.Y - TopLeft.Y;
Double shapeOutlineCentreX = TopLeft.X + (width / 2.0);
Double shapeOutlineCentreY = TopLeft.Y + (height / 2.0);
Double TagCentreOffsetX = CenterOfTag.X - shapeOutlineCentreX;
Double TagCentreOffsetY = CenterOfTag.Y - shapeOutlineCentreY;
Point position = TagLocation.BigBlob.GetCenterPosition(tagCanvas);
Point otherPosition = TagLocation.SmallBlob.GetCenterPosition(tagCanvas);
Vector v = otherPosition - position;
Point centerPoint = position + v / 2;
Double trigCenterX = CenterOfTag.X - TopLeft.X;
Double trigCenterY = CenterOfTag.Y - TopLeft.Y;
**shapeOutline.RenderTransform = new RotateTransform(Math.Atan2(v.Y, v.X) * 180 / Math.PI - TagAngleOffset, trigCenterX, trigCenterY);**
shapeOutline.SetValue(Canvas.LeftProperty, centerPoint.X - (width / 2.0) - TagCentreOffsetX);
shapeOutline.SetValue(Canvas.TopProperty, centerPoint.Y - (height / 2.0) - TagCentreOffsetY);
}

Related

How do I determine the rotation point of a polygon (triangle)?

I would like to draw a thick, transparent arrow with an arrowhead:
Here's the code that draws the arrow shaft. Notice that I have to offset the rectangle so the calculations are done from the midpoint of the rectangle.
private void DrawMovementArrow(bool color, double StartX, double StartY, double EndX, double EndY)
{
SolidColorBrush partiallyTransparentSolidColorBrush;
Rectangle myRectangle = new Rectangle();
// This will be replaced by piece size
int width = 35;
myRectangle.Width = width;
// Apparently necessary to offset the drawing of the path so that the point is in the center of the path; not the edge.
StartX -= width / 2;
EndX -= width / 2;
myRectangle.Height = Map.EuclideanDistance(StartX, StartY, EndX, EndY) ;
int angle = CalculateAngle(StartX , StartY , EndX , EndY );
// This selects the midpoint of edge of the rectangle to rotate around (weird system)
myRectangle.RenderTransformOrigin = new Point(0.5, 0);
angle = angle - 180;
RotateTransform rotateTransform1 = new RotateTransform(angle, 0 , 0 );
myRectangle.RenderTransform = rotateTransform1;
if (color)
partiallyTransparentSolidColorBrush = new SolidColorBrush(Colors.Blue);
else
partiallyTransparentSolidColorBrush = new SolidColorBrush(Colors.Red);
partiallyTransparentSolidColorBrush.Opacity = 0.4;
myRectangle.Fill = partiallyTransparentSolidColorBrush;
MovementCanvas1.Children.Clear();
MovementCanvas1.Children.Add(myRectangle);
Canvas.SetTop(myRectangle, StartY);
Canvas.SetLeft(myRectangle, StartX);
DrawArrowhead(color, EndX, EndY, angle + 90, width);
ShowUnitCenter(MovementArrowList[0]);
}
Note that this code selects a point in the middle of the edge to rotate the rectangle:
// This selects the midpoint of edge of the rectangle to rotate around (weird system)
myRectangle.RenderTransformOrigin = new Point(0.5, 0);
The problem is that I can't find that point with the arrowhead (triangle). Here's the code that draws the arrowhead:
public void DrawArrowhead(bool color, double x, double y, int angle, int width)
{
x += width /2 ;
width = width + (width / 2);
//Add the Polygon Element
Polygon myPolygon = new Polygon();
myPolygon.Opacity = 0.4;
if (color)
{
myPolygon.Fill = new SolidColorBrush(Colors.Blue);
myPolygon.Stroke = System.Windows.Media.Brushes.Blue;
}
else
{
myPolygon.Fill = new SolidColorBrush(Colors.Red);
myPolygon.Stroke = System.Windows.Media.Brushes.Red;
}
myPolygon.StrokeThickness = 0;
RotateTransform rotateTransform1 = new RotateTransform(angle, 0, 0);
myPolygon.RenderTransform = rotateTransform1;
// This selects the midpoint of edge of the triangle to rotate around (weird system)
myPolygon.RenderTransformOrigin = new Point(0.0, 0.5);
System.Windows.Point Point1 = new System.Windows.Point(0, 0);
System.Windows.Point Point2 = new System.Windows.Point(width / 2, width / 2);
System.Windows.Point Point3 = new System.Windows.Point(0,width);
PointCollection myPointCollection = new PointCollection();
myPointCollection.Add(Point1);
myPointCollection.Add(Point2);
myPointCollection.Add(Point3);
myPolygon.Points = myPointCollection;
MovementCanvas1.Children.Add(myPolygon);
Canvas.SetTop(myPolygon, y );
Canvas.SetLeft(myPolygon, x );
}
Note the myPointCollection that creates the triangle. The problem is that I've tried almost every conceivable combination of values in RenderTransformOrigin to find the point that (center bottom of triangle) to use for the rotation point. Nothing seems to be working out.
Can anybody suggest the correct value?
Edit Solved
I solved it by changing the points of the triangle. That was easier than trying to figure out the rotation point.
Changing the points that made up the triangle solved the problem. This was easier than trying to find the rotation point.

How do you translate chart coordinates to device pixels in SkiaSharp?

I'm attempting to write a matrix transform to convert chart points to device pixels in SkiaSharp. I have it functional as long as I use 0,0 as my minimum chart coordinates but if I need to to step up from a negative number, it causes the drawing to shift left and down. That is to say that the X Axis is shifted to the left off the window and the Y Axis is shift down off the window.
This is intended to be a typical line chart (minimum chart point at the lower left while minimum device point at the upper left). I have accounted for that already in the transform.
While stepping through code I can see that the coordinates returned from the Matrix are not what I expect them to be, so I believe the issue to be with my transform but I haven't been able to pinpoint it.
UPDATE: After further examination, I believe I was mistaken, it is not shifted, it's just not scaling properly to the max end of the screen. There is a bigger margin at the top and right side of the chart than there should be, but the bottom and left side are fine. I've been undable to determine why the scaling doesn't fill the canvas.
Below are my matrix methods:
private SKMatrix ChartToDeviceMatrix, DeviceToChartMatrix;
private void ConfigureTransforms(SKPoint ChartMin,
SKPoint ChartMax, SKPoint DeviceMin, SKPoint DeviceMax)
{
this.ChartToDeviceMatrix = SKMatrix.MakeIdentity();
float xScale = (DeviceMax.X - DeviceMin.X) / (ChartMax.X - ChartMin.X);
float yScale = (DeviceMin.Y - DeviceMax.Y) / (ChartMax.Y - ChartMin.Y);
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, DeviceMin.X, DeviceMax.Y);
this.ChartToDeviceMatrix.TryInvert(out this.DeviceToChartMatrix);
}
// Transform a point from chart to device coordinates.
private SKPoint ChartToDevice(SKPoint point)
{
return this.ChartToDeviceMatrix.MapPoint(point);
}
The code invoking this is:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
float strokeWidth = 1;
float margin = 10;
// SKPaint definitions omitted for brevity.
var ChartMin = new SKPoint(-10, -1); // Works fine if I change this to 0,0
var ChartMax = new SKPoint(110, 11);
var DeviceMin = new SKPoint(margin, margin);
var DeviceMax = new SKPoint(info.Width - margin, info.Height - margin);
const float stepX = 10;
const float stepY = 1;
const float tickX = 0.5;
const float tickY = 0.075F;
// Prepare the transformation matrices.
this.ConfigureTransforms(ChartMin, ChartMax, DeviceMin, DeviceMax);
// Draw the X axis.
var lineStart = new SKPoint(ChartMin.X, 0);
var lineEnd = new SKPoint(ChartMax.X, 0);
canvas.DrawLine(this.ChartToDevice(lineStart), this.ChartToDevice(lineEnd), axisPaint);
// X Axis Tick Marks
for (float x = stepX; x <= ChartMax.X - stepX; x += stepX)
{
var tickMin = new SKPoint(x, -tickY);
var tickMax = new SKPoint(x, tickY);
canvas.DrawLine(this.ChartToDevice(tickMin), this.ChartToDevice(tickMax), axisPaint);
}
// Draw the Y axis.
// The inversion of above, basically the same.
I was able to discover my own problem with enough time. I wasn't calculating the offset correct.
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, DeviceMin.X, DeviceMax.X);
Should have been:
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, -ChartMin.X * xScale + DeviceMin.Y, -ChartMin.Y * yScale + DeviceMax.Y);
Final Matrix method was:
private SKMatrix ChartToDeviceMatrix, DeviceToChartMatrix;
private void ConfigureTransforms(SKPoint ChartMin, SKPoint ChartMax, SKPoint DeviceMin, SKPoint DeviceMax)
{
this.ChartToDeviceMatrix = SKMatrix.MakeIdentity();
float xScale = (DeviceMax.X - DeviceMin.X) / (ChartMax.X - ChartMin.X);
float yScale = (DeviceMin.Y - DeviceMax.Y) / (ChartMax.Y - ChartMin.Y);
float xOffset = -ChartMin.X * xScale + DeviceMin.X;
float yOffset = -ChartMin.Y * yScale + DeviceMax.Y;
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, xOffset, yOffset);
this.ChartToDeviceMatrix.TryInvert(out this.DeviceToChartMatrix);
}

How to draw an arc on WritableBitmap + WritablebitmapEx

I'm drawing a lot of shapes on WritableBitmap with help of WritableBitmapEx in my WPF application.
Unfortunately there is no ready to go function to draw an arc on it.
How can I:
1. Draw an arc on WritableBitmap?
2. Draw an anti-aliased arc with variable thickness on WritableBitmap?
I just need to draw circular arcs.
There is possibility to draw a nice, anti-aliased arc with variable thickness (System.Windows.Media.ArcSegment) on Canvas - but with thousands of shapes the performance of Canvas is poor - that's why I'm using WritableBitmap.
If it would be needed by some algorithms I have already calculated arc parameters like:
CenterPoint, Radius, StartPoint, EndPoint, StartAngle, EndAngle, ArcLength, IsLarge or Direction
I was trying to draw it manually with code similar to this:
int number_of_points = 1000;
for(int i=0; i<=number_of_points; i++){
double progress=(double)i/number_of_points;
double theta = (StartAngle + ArcLength * progress) * Math.PI / 180.0;
draw_pixel(
Center.X + Radius * Math.Cos(theta),
Center.Y + Radius * Math.Sin(theta)
);
}
but with varying resolution of picture, varying size of arc (how to calculate optimum number_of_points?), varying thickness of arc and with anti aliasing it starts to be a little tricky.
1. Draw an arc on WritableBitmap?
After analyzing mono libgdiplus sources on github I found that they are drawing an arc using Bezier curve.
I have ported some of their functions to c#.
DrawArc extension function can be used (with help of DrawBezier from WritableBitmapEx) to draw an simple arc.
There is no anti-aliased version of DrawBezier in WritableBitmapEx so this solution answers (only) my first question:
namespace System.Windows.Media.Imaging
{
public static partial class WriteableBitmapArcExtensions
{
//port of mono libgdiplus function
//append_arcs (GpPath *path, float x, float y, float width, float height, float startAngle, float sweepAngle)
//from: https://github.com/mono/libgdiplus/blob/master/src/graphics-path.c
public static void DrawArc(this WriteableBitmap bmp, float x, float y, float width, float height, float startAngle, float sweepAngle, Color color)
{
int i;
float drawn = 0;
int increment;
float endAngle;
bool enough = false;
if (Math.Abs(sweepAngle) >= 360)
{
bmp.DrawEllipse((int)x, (int)y, (int)width, (int)height, color);
return;
}
endAngle = startAngle + sweepAngle;
increment = (endAngle < startAngle) ? -90 : 90;
/* i is the number of sub-arcs drawn, each sub-arc can be at most 90 degrees.*/
/* there can be no more then 4 subarcs, ie. 90 + 90 + 90 + (something less than 90) */
for (i = 0; i < 4; i++)
{
float current = startAngle + drawn;
float additional;
if (enough)
return;
additional = endAngle - current; /* otherwise, add the remainder */
if (Math.Abs(additional) > 90)
{
additional = increment;
}
else
{
/* a near zero value will introduce bad artefact in the drawing */
if ((additional >= -0.0001f) && (additional <= 0.0001f))
return;
enough = true;
}
bmp._DrawArc(
x, y,
width, height, /* bounding rectangle */
current, current + additional, color);
drawn += additional;
}
}
//port of mono libgdiplus function
//append_arc (GpPath *path, BOOL start, float x, float y, float width, float height, float startAngle, float endAngle)
//from: https://github.com/mono/libgdiplus/blob/master/src/graphics-path.c
private static void _DrawArc(this WriteableBitmap bmp, float x, float y, float width, float height, float startAngle, float endAngle, Color color)
{
double sin_alpha, sin_beta, cos_alpha, cos_beta;
var rx = width / 2;
var ry = height / 2;
/* center */
var cx = x + rx;
var cy = y + ry;
/* angles in radians */
var alpha = startAngle * Math.PI / 180;
var beta = endAngle * Math.PI / 180;
/* adjust angles for ellipses */
alpha = Math.Atan2(rx * Math.Sin(alpha), ry * Math.Cos(alpha));
beta = Math.Atan2(rx * Math.Sin(beta), ry * Math.Cos(beta));
if (Math.Abs(beta - alpha) > Math.PI)
{
if (beta > alpha)
beta -= 2 * Math.PI;
else
alpha -= 2 * Math.PI;
}
var delta = beta - alpha;
// http://www.stillhq.com/ctpfaq/2001/comp.text.pdf-faq-2001-04.txt (section 2.13)
var bcp = 4.0 / 3 * (1 - Math.Cos(delta / 2)) / Math.Sin(delta / 2);
sin_alpha = Math.Sin(alpha);
sin_beta = Math.Sin(beta);
cos_alpha = Math.Cos(alpha);
cos_beta = Math.Cos(beta);
/* starting point */
double sx = cx + rx * cos_alpha;
double sy = cy + ry * sin_alpha;
//DrawBezier comes from WritableBitmapEx library
bmp.DrawBezier(
(int)(sx),
(int)(sy),
(int)(cx + rx * (cos_alpha - bcp * sin_alpha)),
(int)(cy + ry * (sin_alpha + bcp * cos_alpha)),
(int)(cx + rx * (cos_beta + bcp * sin_beta)),
(int)(cy + ry * (sin_beta - bcp * cos_beta)),
(int)(cx + rx * cos_beta),
(int)(cy + ry * sin_beta),
color
);
}
}
}
I have commented an issue on WritableBitmapEx site: I would like to draw arcs - so maybe part of this code would be included in WritableBitmapEx library.
2. Draw an anti-aliased arc with variable thickness on WritableBitmap?
After reading comment from ForeverZer0 I have made some experiments with System.Drawing.Graphics and WritableBitmap. With help of getting a DrawingContext for a wpf WriteableBitmap I have done it with such code:
WritableBitmap ret = BitmapFactory.New(img_width, img_height);
ret.Lock();
var bmp = new System.Drawing.Bitmap(
ret.PixelWidth,
ret.PixelHeight,
ret.BackBufferStride,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb,
ret.BackBuffer
);
System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
g.DrawArc(...); //<-- draws an antialiased arc with variable thickness
g.Dispose();
bmp.Dispose();
ret.AddDirtyRect(new Int32Rect(0, 0, ret.PixelWidth, ret.PixelHeight));
ret.Unlock();
return ret; //<-- WritableBitmap with beautifull arc on it;

How do I properly setup a texture position using XNA/Monogame VertexPositionTexture on a circle

I am using the following to create a circle using VertexPositionTexture:
public static ObjectData Circle(Vector2 origin, float radius, int slices)
{
/// See below
}
The texture that is applied to it doesn't look right, it spirals out from the center. I have tried some other things but nothing does it how I want. I would like for it to kind-of just fan around the circle, or start in the top-left end finish in the bottom-right. Basically wanting it to be easier to create textures for it.
I know that are MUCH easier ways to do this without using meshes, but that is not what I am trying to accomplish right now.
This is the code that ended up working thanks to Pinckerman:
public static ObjectData Circle(Vector2 origin, float radius, int slices)
{
VertexPositionTexture[] vertices = new VertexPositionTexture[slices + 2];
int[] indices = new int[slices * 3];
float x = origin.X;
float y = origin.Y;
float deltaRad = MathHelper.ToRadians(360) / slices;
float delta = 0;
float thetaInc = (((float)Math.PI * 2) / vertices.Length);
vertices[0] = new VertexPositionTexture(new Vector3(x, y, 0), new Vector2(.5f, .5f));
float sliceSize = 1f / slices;
for (int i = 1; i < slices + 2; i++)
{
float newX = (float)Math.Cos(delta) * radius + x;
float newY = (float)Math.Sin(delta) * radius + y;
float textX = 0.5f + ((radius * (float)Math.Cos(delta)) / (radius * 2));
float textY = 0.5f + ((radius * (float)Math.Sin(delta)) /(radius * 2));
vertices[i] = new VertexPositionTexture(new Vector3(newX, newY, 0), new Vector2(textX, textY));
delta += deltaRad;
}
indices[0] = 0;
indices[1] = 1;
for (int i = 0; i < slices; i++)
{
indices[3 * i] = 0;
indices[(3 * i) + 1] = i + 1;
indices[(3 * i) + 2] = i + 2;
}
ObjectData thisData = new ObjectData()
{
Vertices = vertices,
Indices = indices
};
return thisData;
}
public static ObjectData Ellipse()
{
ObjectData thisData = new ObjectData()
{
};
return thisData;
}
ObjectData is just a structure that contains an array of vertices & an array of indices.
Hope this helps others that may be trying to accomplish something similar.
It looks like a spiral because you've set the upper-left point for the texture Vector2(0,0) in the center of your "circle" and it's wrong. You need to set it on the top-left vertex of the top-left slice of you circle, because 0,0 of your UV map is the upper left corner of your texture.
I think you need to set (0.5, 0) for the upper vertex, (1, 0.5) for the right, (0.5, 1) for the lower and (0, 0.5) for the left, or something like this, and for the others use some trigonometry.
The center of your circle has to be Vector2(0.5, 0.5).
Regarding the trigonometry, I think you should do something like this.
The center of your circle has UV value of Vector2(0.5, 0.5), and for the others (supposing the second point of the sequence is just right to the center, having UV value of Vector2(1, 0.5)) try something like this:
vertices[i] = new VertexPositionTexture(new Vector3(newX, newY, 0), new Vector2(0.5f + radius * (float)Math.Cos(delta), 0.5f - radius * (float)Math.Sin(delta)));
I've just edited your third line in the for-loop. This should give you the UV coordinates you need for each point. I hope so.

How to retrieve zoom factor of a WinForms PictureBox?

I need the precise position of my mouse pointer over a PictureBox.
I use the MouseMove event of the PictureBox.
On this PictureBox, I use the "zoom" property to show an image.
What is the correct way for getting the position of the mouse on the original (unzoomed) image?
Is there a way to find the scale factor and use it?
I think need to use imageOriginalSize/imageShowedSize to retrieve this scale factor.
I use this function:
float scaleFactorX = mypic.ClientSize.Width / mypic.Image.Size.Width;
float scaleFactorY = mypic.ClientSize.Height / mypic.Image.Size.Height;
Is possible to use this value to get the correct position of the cursor over the image?
I had to solve this same problem today. I wanted it to work for images of any width:height ratio.
Here's my method to find the point 'unscaled_p' on the original full-sized image.
Point p = pictureBox1.PointToClient(Cursor.Position);
Point unscaled_p = new Point();
// image and container dimensions
int w_i = pictureBox1.Image.Width;
int h_i = pictureBox1.Image.Height;
int w_c = pictureBox1.Width;
int h_c = pictureBox1.Height;
The first trick is to determine if the image is a horizontally or vertically larger relative to the container, so you'll know which image dimension fills the container completely.
float imageRatio = w_i / (float)h_i; // image W:H ratio
float containerRatio = w_c / (float)h_c; // container W:H ratio
if (imageRatio >= containerRatio)
{
// horizontal image
float scaleFactor = w_c / (float)w_i;
float scaledHeight = h_i * scaleFactor;
// calculate gap between top of container and top of image
float filler = Math.Abs(h_c - scaledHeight) / 2;
unscaled_p.X = (int)(p.X / scaleFactor);
unscaled_p.Y = (int)((p.Y - filler) / scaleFactor);
}
else
{
// vertical image
float scaleFactor = h_c / (float)h_i;
float scaledWidth = w_i * scaleFactor;
float filler = Math.Abs(w_c - scaledWidth) / 2;
unscaled_p.X = (int)((p.X - filler) / scaleFactor);
unscaled_p.Y = (int)(p.Y / scaleFactor);
}
return unscaled_p;
Note that because Zoom centers the image, the 'filler' length has to be factored in to determine the dimension that is not filled by the image. The result, 'unscaled_p', is the point on the unscaled image that 'p' correlates to.
Hope that helps!
If I have understood you correctly I believe you would want to do something of this nature:
Assumption: the PictureBox fits to the image width/height, there is no space between the border of the PictureBox and the actual image.
ratioX = e.X / pictureBox.ClientSize.Width;
ratioY = e.Y / pictureBox.ClientSize.Height;
imageX = image.Width * ratioX;
imageY = image.Height * ratioY;
this should give you the points ot the pixel in the original image.
Here is a simple function to solve this:
private Point RemapCursorPosOnZoomedImage(PictureBox pictureBox, int x, int y, out bool isInImage)
{
// original size of image in pixel
float imgSizeX = pictureBox.Image.Width;
float imgSizeY = pictureBox.Image.Height;
// current size of picturebox (without border)
float cSizeX = pictureBox.ClientSize.Width;
float cSizeY = pictureBox.ClientSize.Height;
// calculate scale factor for both sides
float facX = (cSizeX / imgSizeX);
float facY = (cSizeY / imgSizeY);
// use smaller one to fit picturebox zoom layout
float factor = Math.Min(facX, facY);
// calculate current size of the displayed image
float rSizeX = imgSizeX * factor;
float rSizeY = imgSizeY * factor;
// calculate offsets because image is centered
float startPosX = (cSizeX - rSizeX) / 2;
float startPosY = (cSizeY - rSizeY) / 2;
float endPosX = startPosX + rSizeX;
float endPosY = startPosY + rSizeY;
// check if cursor hovers image
isInImage = true;
if (x < startPosX || x > endPosX) isInImage = false;
if (y < startPosY || y > endPosY) isInImage = false;
// remap cursor position
float cPosX = ((float)x - startPosX) / factor;
float cPosY = ((float)y - startPosY) / factor;
// create new point with mapped coords
return new Point((int)cPosX, (int)cPosY);
}

Categories

Resources