C# GDI+ clip draw and more - c#

I am writing an application that has a panel in which I display:
Picture background
Lots of drawn objects (using GDI+ lines, paths, etc)
Said objects have some hit detection in mouse move event.
The panel pans and zooms. The panel is double buffered.
So everything is working pretty well - things look good, no flicker, but I am at the point where I am trying to focus on performance. I take less than 1 ms to draw my objects (per object) BUT when zoomed out I can have upwards of 500 objects to draw, which starts to make everything from the drawing to the hit detection sluggish.
I have already done a few things to try to improve performance like making a list of only the on screen (drawable) objects - but as previously mentioned, when zoomed out, it can still be LOTS. I got the grand idea that maybe instead of updating EVERY object EVERY time, I could implement a simple Invalidate() which would tell the object its made a visual change worthy of drawing. In the object paint code (which is passed e.graphics), I attempted to set the clipping region to the size of the object and only update that portion of the panel BG (also when done, I reset the e.Grpahics.clip). The result was only the invalidated object paints - everything else is blank.
I am confident this is the C#/GDI noob coming out in me and you all will tell me what stupid mistake I've probably overlooked.
At the time of writing this, I realize that I can save some time by make the path of my object constant instead of making it fresh every time in the draw function (since all objects paths are identical). I'll update with time I shave off shortly.
Here is the code that paints the BG image (and determines which OBJS to paint) and the OBJ paint code.
private void PaintImage(PaintEventArgs e)
{
int scale = 10; //TARGET ICON BASE SCALE....
var watch2 = System.Diagnostics.Stopwatch.StartNew();
if (bitmap != null && redrawBG)
{
float widthZoomed = TgtPanel.Width / Zoom;
float heigthZoomed = TgtPanel.Height / Zoom;
//Do checks the reason 30,000 is used is because
//much over this will cause DrawImage to crash
if (widthZoomed > 30000.0f)
{
Zoom = TgtPanel.Width / 30000.0f;
widthZoomed = 30000.0f;
}
if (heigthZoomed > 30000.0f)
{
Zoom = TgtPanel.Height / 30000.0f;
heigthZoomed = 30000.0f;
}
//we stop at 2 because at this point you have almost zoomed into a single pixel
if (widthZoomed < 2.0f)
{
Zoom = TgtPanel.Width / 2.0f;
widthZoomed = 2.0f;
}
if (heigthZoomed < 2.0f)
{
Zoom = TgtPanel.Height / 2.0f;
heigthZoomed = 2.0f;
}
float wz2 = widthZoomed / 2.0f;
float hz2 = heigthZoomed / 2.0f;
Rectangle drawRect = new Rectangle(
(int)(viewPortCenter.X - wz2),
(int)(viewPortCenter.Y - hz2),
(int)(widthZoomed),
(int)(heigthZoomed));
e.Graphics.Clear(Color.White); //Clear the Back buffer
//Draw the image, Write image to back buffer, and [render back buffer - no longer using my own backbuffer]
e.Graphics.DrawImage(bitmap,
this.TgtPanel.DisplayRectangle, drawRect, GraphicsUnit.Pixel);
// e.Graphics.ScaleTransform(Zoom, Zoom);
// e.Graphics.DrawImage(bitmap,
// 0,0);
if (draging)
{ //line to visualize the drag
e.Graphics.DrawLine(new Pen(Color.Yellow, 10), StartDrag.X, StartDrag.Y, lastMouse.X, lastMouse.Y);
}
//this checks for offscreen - works
if (drawRect.X > iconbitmap.Width || drawRect.X < -(drawRect.Width) ||
drawRect.Y > 0 + iconbitmap.Height || drawRect.Y < -(drawRect.Height))
{
label1.Text = "OFF";
}
} //if bitmap != null & redraw
// Font and Brush for the text graphics
Point mypoint = WorldToScreen(0.75f * scale, 7.0f * scale);
RectangleF bgrect = new RectangleF();
bgrect.X = mypoint.X;
bgrect.Y = mypoint.Y;
bgrect.Width = (3.5f * scale * Zoom);
bgrect.Height = (2.0f * scale * Zoom);
int aFontSizeDefault = 40;
int aFontSizeMinimum = 2;
String adrawString = "AAA"; //test this length
Font aDefaultFont = new Font("MS Sans Serif", aFontSizeDefault, FontStyle.Regular);
FontAdjustment afa = new FontAdjustment();
Font AAdjustedFont = afa.GetAdjustedFont(e.Graphics, adrawString,
aDefaultFont, Convert.ToInt32(bgrect.Width), aFontSizeDefault, aFontSizeMinimum, true);
//DRAW TGT BG
var Point1 = ScreenToWorld(0, 0);
var Point2 = ScreenToWorld(TgtPanel.Width, TgtPanel.Height);
//getVisible Screen == on;y draw visible targets
if (redrawBG)
{
VISIBLETARGETS.Clear(); //erase visible tgts array - we're going to update it
foreach (Target TGT in THETARGETS)
if (TGT.PosX >= Point1.X - 40 && TGT.PosX <= Point2.X - 9)
if (TGT.PosY >= Point1.Y - 83 && TGT.PosY <= Point2.Y - 5)
{
TGT.OnScreen = true;
//drawTarget(TGT, AAdjustedFont, e);
VISIBLETARGETS.Add(TGT); //update as visible
}
else TGT.OnScreen = false;
//redrawBG = false;
}
var watch = System.Diagnostics.Stopwatch.StartNew();
foreach(Target TGT in VISIBLETARGETS)
{
if(TGT.Invalidated || redrawBG) // THIS IS DRAWING ONLY TGT -- NOT OTHERS, OR BG - FIX THIS WITH CLIPPING?
drawTarget(TGT, AAdjustedFont, e);
}
watch.Stop();
watch2.Stop();
var elapsedMs = watch.ElapsedMilliseconds;//ElapsedTicks;
label6.Text = "TotalDrawTime = " + watch2.ElapsedMilliseconds.ToString();
label4.Text = "AvgDrawTime = " + elapsedMs.ToString();
label5.Text = "VisibleTgts = " + VISIBLETARGETS.Count.ToString();
AAdjustedFont.Dispose();
aDefaultFont.Dispose();
//------------- DRAWING TGT WITH GDI - WITH ORIGINAL BACKBUFFER
/// myBuffer.Render(this.TgtPanel.CreateGraphics());
redrawBG = false;
}
public void drawTarget(Target Tgt, Font AAdjustedFont, PaintEventArgs e)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
const float scale = 10; //10 is at 1 zoom
var bgrect = new RectangleF();
Point mypoint = WorldToScreen(Tgt.PosX + 0.75f * scale, Tgt.PosY + 1.0f * scale);
bgrect.X = mypoint.X;
bgrect.Y = mypoint.Y;
bgrect.Width = 3.5f * scale * Zoom;
bgrect.Height = 7.5f * scale * Zoom;
//PLAY WITH CLIP
e.Graphics.Clip = new Region(bgrect);
//var hbrush = new HatchBrush(HatchStyle.DarkDownwardDiagonal, Color.White);
var hbrush = new SolidBrush(Color.White);
//if(WantToDrawIconBG() ....
//e.Graphics.FillRectangle(hbrush, bgrect); //ICON BACKGROUND
//ADDR RECT
// mypoint = WorldToScreen(0, Tgt.PosY + 7.0f * scale);
mypoint = WorldToScreen(0, Tgt.PosY + 6.90f * scale); //moved Y up a bit from above
bgrect.Y = mypoint.Y;
bgrect.Height = 1.5f * scale * Zoom;
/////brush.Color = (Color.GhostWhite);
e.Graphics.FillRectangle(hbrush, bgrect);
hbrush.Dispose();
string adrawString = Tgt.Address;
System.Drawing.Font adrawFont = new System.Drawing.Font("Arial", 16);
System.Drawing.SolidBrush adrawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Red);
System.Drawing.StringFormat adrawFormat = new System.Drawing.StringFormat();
adrawFormat.Alignment = StringAlignment.Center;
e.Graphics.DrawString(adrawString, AAdjustedFont, adrawBrush, bgrect, adrawFormat); //draw addr
//======= LETS MAKE THE TGT ICON SHAPE =======
GraphicsPath path = new GraphicsPath(FillMode.Alternate);
//TARGET POINTS (w = 3, h = 6)
var h1 = WorldToScreen(Tgt.PosX + 2 * scale, Tgt.PosY + 1.1f * scale);
var h2 = WorldToScreen(Tgt.PosX + 3 * scale, Tgt.PosY + 1.1f * scale);
var n1 = WorldToScreen(Tgt.PosX + 2 * scale, Tgt.PosY + 2 * scale);
var n2 = WorldToScreen(Tgt.PosX + 3 * scale, Tgt.PosY + 2 * scale);
var s1 = WorldToScreen(Tgt.PosX + 1 * scale, Tgt.PosY + 3 * scale);
var s2 = WorldToScreen(Tgt.PosX + 4 * scale, Tgt.PosY + 3 * scale);
var b1 = WorldToScreen(Tgt.PosX + 1 * scale, Tgt.PosY + 7 * scale);
var b2 = WorldToScreen(Tgt.PosX + 4 * scale, Tgt.PosY + 7 * scale);
var controln2 = WorldToScreen(Tgt.PosX + 1 * scale, (Convert.ToInt32(Tgt.PosY + 0.5 * scale)));
var controls2 = WorldToScreen(Tgt.PosX + 1 * scale, Tgt.PosY + 1 * scale);
Pen pen = new Pen(Color.FromArgb(255, 0, 0, 255));
PointF[] npoints = { n2, s2, b2 };
PointF[] npoints2 = { n1, s1, b1 };
e.Graphics.DrawCurve(pen, npoints, 0.5f);
path.AddCurve(npoints, 0.5f); /////
e.Graphics.DrawLine(pen, b2, b1);
path.AddLine(b2, b1); /////
e.Graphics.DrawCurve(pen, npoints2, 0.5f);
path.AddCurve(npoints2, 0.5f); /////
PointF[] hpoints = { n1, h1, h2, n2 };
e.Graphics.DrawCurve(pen, hpoints, 0.1f);
path.AddCurve(hpoints, 0.1f); /////
path.CloseAllFigures();
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (Zoom > 0.9) //only draw stroke if big enough to see (and there arent a million targets to draw
{
pen.Color = Tgt.Selected ? Color.Chartreuse : Color.FromName(comboBox1.Text); //black default
pen.Width = Tgt.Selected ? 2 * Zoom : 1 * Zoom; //draw thicker border if selected
e.Graphics.DrawPath(pen, path); ///------------------------------------------STROKE PATH.....
//e.Graphics.FillPath(new SolidBrush(Color.ForestGreen), path);
}
// how much time is wasted making 2 gradient brushes? One we wont even use.
LinearGradientBrush linGrBrush = new LinearGradientBrush(
WorldToScreen(Tgt.PosX + 0 * scale, Tgt.PosY + 5 * scale),
WorldToScreen(Tgt.PosX + 5.5f * scale, Tgt.PosY + 5 * scale),
Color.ForestGreen, // Opaque red
Color.FromArgb(255, 0, 255, 0)); // Opaque blue
LinearGradientBrush linRedBrush = new LinearGradientBrush(
WorldToScreen(Tgt.PosX + 0 * scale, Tgt.PosY + 5 * scale),
WorldToScreen(Tgt.PosX + 5.5f * scale, Tgt.PosY + 5 * scale),
Color.FromArgb(255, 255, 0, 0), // Opaque red
Color.Firebrick); // Opaque blue
//FILL TARGET ICON WITH COLOR - UP or DOWN
if (Tgt.IsUp) e.Graphics.FillPath(linGrBrush, path);
else e.Graphics.FillPath(linRedBrush, path);
//------------
//tgt lines (cosmetic only)
if (Zoom > 0.9) //only draw if big enough to see (and there arent a million targets to draw
{
var transPen = new Pen(Color.FromArgb(150, 200, 200, 200));
var l1a = WorldToScreen(Tgt.PosX + 2.5f * scale, Tgt.PosY + 1.5f * scale);
var l1b = WorldToScreen(Tgt.PosX + 2.5f * scale, Tgt.PosY + 6 * scale);
e.Graphics.DrawLine(transPen, l1a, l1b);
var l2a = WorldToScreen(Tgt.PosX + 1.5f * scale, Tgt.PosY + 2.5f * scale);
var l2b = WorldToScreen(Tgt.PosX + 1.5f * scale, Tgt.PosY + 6.5f * scale);
e.Graphics.DrawLine(transPen, l2a, l2b);
var l3a = WorldToScreen(Tgt.PosX + 3.5f * scale, Tgt.PosY + 2.5f * scale);
var l3b = WorldToScreen(Tgt.PosX + 3.5f * scale, Tgt.PosY + 6.5f * scale);
e.Graphics.DrawLine(transPen, l3a, l3b);
}
//Draw Hits....
mypoint = WorldToScreen(Tgt.PosX + 1.0f * scale, Tgt.PosY + 3.0f * scale);
bgrect.X = mypoint.X;
bgrect.Y = mypoint.Y;
bgrect.Width = 3.0f * scale * Zoom;
bgrect.Height = 1.5f * scale * Zoom;
adrawString = Tgt.Hits.ToString();
adrawFormat.Alignment = StringAlignment.Center;
if (Zoom > 0.9) //only draw if big enough to see (and there arent a million targets to draw
{
adrawBrush.Color = Color.FromArgb(100, 100, 100, 100);
e.Graphics.FillRectangle(adrawBrush, bgrect);
}
adrawBrush.Color = Color.White;
e.Graphics.DrawString(adrawString, AAdjustedFont, adrawBrush, bgrect, adrawFormat); //draw hits
//Draw Score....
mypoint = WorldToScreen(Tgt.PosX + 1.0f * scale, Tgt.PosY + 5.0f * scale);
bgrect.X = mypoint.X;
bgrect.Y = mypoint.Y;
bgrect.Width = 3.0f * scale * Zoom;
bgrect.Height = 1.5f * scale * Zoom;
adrawString = Tgt.Score.ToString();
adrawFormat.Alignment = StringAlignment.Center;
if (Zoom > 0.9) //only draw if big enough to see (and there arent a million targets to draw
{
adrawBrush.Color = Color.FromArgb(100, 100, 100, 100);
e.Graphics.FillRectangle(adrawBrush, bgrect);
}
adrawBrush.Color = Color.White;
e.Graphics.DrawString(adrawString, AAdjustedFont, adrawBrush, bgrect, adrawFormat); //draw hits
adrawFont.Dispose();
adrawBrush.Dispose();
adrawFormat.Dispose();
path.Dispose();
watch.Stop();
var elapsedMs = watch.ElapsedTicks;
//14279 original ticks
//12764 removing label and reducing font size calcs...
// 1695 ! removed font size to external calc so it happens only once
e.Graphics.ResetClip();
e.Graphics.Clip.Dispose();
Tgt.Invalidated = false; //dont draw again until change
}

Related

How do I convert from mouse coordinates to pixel coordinates of a TransformedBitmap?

My question is similar to How to get correct position of pixel from mouse coordinates?, with the added caveat that the image is potentially a TransformedBitmap, where flips and rotations could be applied, and still return the pixel coordinates of the original image.
My Window's design looks like this:
<DockPanel>
<Label DockPanel.Dock="Bottom" Name="TheLabel" />
<Image DockPanel.Dock="Top" Name="TheImage" Stretch="Uniform" RenderOptions.BitmapScalingMode="NearestNeighbor" MouseMove="TheImage_MouseMove" />
</DockPanel>
and the codebehind looks like this:
public MainWindow()
{
InitializeComponent();
const int WIDTH = 4;
const int HEIGHT = 3;
byte[] pixels = new byte[WIDTH * HEIGHT * 3];
pixels[0] = Colors.Red.B;
pixels[1] = Colors.Red.G;
pixels[2] = Colors.Red.R;
pixels[(WIDTH * (HEIGHT - 1) + (WIDTH - 1)) * 3 + 0] = Colors.Blue.B;
pixels[(WIDTH * (HEIGHT - 1) + (WIDTH - 1)) * 3 + 1] = Colors.Blue.G;
pixels[(WIDTH * (HEIGHT - 1) + (WIDTH - 1)) * 3 + 2] = Colors.Blue.R;
BitmapSource bs = BitmapSource.Create(WIDTH, HEIGHT, 96.0, 96.0, PixelFormats.Bgr24, null, pixels, WIDTH * 3);
TheImage.Source = bs;
}
private void TheImage_MouseMove(object sender, MouseEventArgs e)
{
Point p = e.GetPosition(TheImage);
if (TheImage.Source is TransformedBitmap tb)
TheLabel.Content = tb.Transform.Inverse.Transform(new Point(p.X * tb.PixelWidth / TheImage.ActualWidth, p.Y * tb.PixelHeight / TheImage.ActualHeight)).ToString();
else if (TheImage.Source is BitmapSource bs)
TheLabel.Content = new Point(p.X * bs.PixelWidth / TheImage.ActualWidth, p.Y * bs.PixelHeight / TheImage.ActualHeight).ToString();
}
When hovering in the bottom-right corner (which I colored blue for easy tracking) of the non-transformed image, you correctly see the coordinates of (~4, ~3), which are the image dimensions.
However, once you apply a transform, for example changing TheImage.Source = bs; to TheImage.Source = new TransformedBitmap(bs, new RotateTransform(90.0));, hovering over the blue instead gives (~4, ~0).
I think one could look at the actual matrix values of the transform, and determine how to adjust the point in all the various cases, but it seems like there should be an easier solution using the inverse transform.
When an Image element renders a TransformedBitmap, the center point of the Transform is effectively ignored and the bitmap is shifted into an appropriate coordinate range.
Since this shift is not applied to the transfomation of a Point, you have to compensate it, e.g. like this:
var p = e.GetPosition(TheImage);
if (TheImage.Source is BitmapSource bs)
{
p = new Point(
p.X * bs.PixelWidth / TheImage.ActualWidth,
p.Y * bs.PixelHeight / TheImage.ActualHeight);
if (TheImage.Source is TransformedBitmap tb)
{
var w = tb.Source.PixelWidth;
var h = tb.Source.PixelHeight;
p = tb.Transform.Inverse.Transform(p);
p = new Point((p.X + w) % w, (p.Y + h) % h);
}
TheLabel.Content = $"x: {(int)p.X}, y: {(int)p.Y}";
}
The above code will however not work correctly when the Transform center is set to a value other than (0,0).
In order to make it work for Transforms with arbitrary center point, you have to clear the offset values in the transform matrix:
var p = e.GetPosition(TheImage);
if (TheImage.Source is BitmapSource bs)
{
p = new Point(
p.X * bs.PixelWidth / TheImage.ActualWidth,
p.Y * bs.PixelHeight / TheImage.ActualHeight);
if (TheImage.Source is TransformedBitmap tb)
{
var w = tb.Source.PixelWidth;
var h = tb.Source.PixelHeight;
var t = tb.Transform.Value;
t.Invert();
t.OffsetX = 0d;
t.OffsetY = 0d;
p = t.Transform(p);
p = new Point((p.X + w) % w, (p.Y + h) % h);
}
TheLabel.Content = $"x: {(int)p.X}, y: {(int)p.Y}";
}

Detect passing of rectangle over yellow pixel

I have a query regarding the best approach to detect when a moving and potentially rotated rectangle passes over a yellow pixel of a Panel's background image.
I have a method which accepts an Image and a Point, and returns true if that point is that of a yellow pixel. I require this colour detection for the function of my game, which resets the car (player) if it drives over the yellow borders of the track. This method is shown below:
private Boolean isYellow(Image image, Point point)
{
Bitmap bitmap = new Bitmap(image);
Color color = bitmap.GetPixel(point.X, point.Y);
return (color.R > 220 && color.G > 220 && color.B < 200);
}
Previously, to detect if the player rectangle passes over yellow, I checked against the location of the rectangle, as provided by the X and Y values of the object. The issue with this is that the location is the top left corner of a horizontal rectangle, meaning the car can drive almost entirely off the track without detection occurring.
I'd like to fix this by checking all points covered by the rectangle. This is not as simple as it may seem as the rectangle is likely to be rotated. My drawing and movement logic is shown below:
public void draw(Graphics g)
{
int dx = rectangle.X + (rectangle.Height / 2);
int dy = rectangle.Y + (rectangle.Width / 2);
g.ScaleTransform(xScale, yScale);
g.TranslateTransform(dx, dy);
g.RotateTransform((float) ((180 * angle) / Math.PI));
g.TranslateTransform(-dx, -dy);
g.DrawImage(image, rectangle.X, rectangle.Y);
g.ResetTransform();
}
public void move(uRaceGame game, Panel panel)
{
double cos = Math.Cos(angle), sin = Math.Sin(angle);
int xLocation = 200;
int yLocation = 200;
xLocation = (int) Math.Floor(rectangle.X + (cos * game.moveDir * 60));
yLocation = (int) Math.Floor(rectangle.Y + (sin * game.moveDir * 60));
angle = (angle + (game.rotateDir * (Math.PI / 128))) % (Math.PI * 2);
if (xLocation * xScale > panel.Width - (rectangle.Width * cos) || yLocation * yScale > panel.Height - (rectangle.Width * sin) - 5 || xLocation * xScale < 0 || yLocation * yScale < 5) return;
rectangle.Location = new Point(xLocation, yLocation);
}
I tried but failed to create a method which translates the coords of the corner and figures out the middle of the rectangle, but this does not work, and the yellow detection fires in very obscure places:
public Point getCentre()
{
int cX = (int) (rectangle.X + ((rectangle.Width / 2) / xScale)), cY = (int) (rectangle.Y + ((rectangle.Height / 2) / yScale));
float tempX = (rectangle.X - cX), tempY = (rectangle.Y - cY);
double rX = (tempX * Math.Cos(angle)) - (tempY * Math.Sin(angle));
double rY = (tempX * Math.Sin(angle)) - (tempY * Math.Cos(angle));
return new Point((int) ((rX + cX) * xScale), (int) ((rY + cY) * yScale));
}
I'd really appreciate any suggestions on how to tackle this. I included the translation and yellow detection code in case I'm miles off in my attempt and someone else has a better idea.
Thank you very much.
There are two approaches that come to my mind:
You can create loops that go along the tilted sides of the car rectangle
Or you can copy the car to an untilted bitmap and loop over it normally.
Here is an example of the second approach.
It uses a LockBits method that detects Yellow with your code in a Bitmap.
And it prepares that bitmap by copying it from the original BackgroundImage un-rotated.
Here is the result, including a control Panel that shows the untilted Rectangle:
Here is the yellow finder function. It uses Lockbits for speed:
using System.Runtime.InteropServices;
using System.Drawing.Imaging;
public bool testForYellowBitmap(Bitmap bmp)
{
Size s1 = bmp.Size;
PixelFormat fmt = new PixelFormat();
fmt = bmp.PixelFormat;
Rectangle rect = new Rectangle(0, 0, s1.Width, s1.Height);
BitmapData bmp1Data = bmp.LockBits(rect, ImageLockMode.ReadOnly, fmt);
byte bpp1 = 4;
if (fmt == PixelFormat.Format24bppRgb) bpp1 = 3;
else if (fmt == PixelFormat.Format32bppArgb) bpp1 = 4; else return false; // throw!!
int size1 = bmp1Data.Stride * bmp1Data.Height;
byte[] data1 = new byte[size1];
System.Runtime.InteropServices.Marshal.Copy(bmp1Data.Scan0, data1, 0, size1);
for (int y = 0; y < s1.Height; y++)
{
for (int x = 0; x < s1.Width; x++)
{
Color c1;
int index1 = y * bmp1Data.Stride + x * bpp1;
if (bpp1 == 4)
c1 = Color.FromArgb(data1[index1 + 3], data1[index1 + 2],
data1[index1 + 1], data1[index1 + 0]);
else c1 = Color.FromArgb(255, data1[index1 + 2],
data1[index1 + 1], data1[index1 + 0]);
if (c1.R > 220 && c1.G > 220 && c1.B < 200)
{ bmp.UnlockBits(bmp1Data); return true; }
}
}
bmp.UnlockBits(bmp1Data);
return false;
}
I prepare the Bitmap to compare in the MouseMove. The variables w, h, w2, h2 hold the width, height and halves of that of the car's size. The source bitmap is in drawPanel1.BackgroundImage. The current angle is in a TrackBar tr_a.Value. For further control I also display the rotated car rectangle in White.
private void drawPanel1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button.HasFlag(MouseButtons.Left))
{
Size sz = drawPanel1.BackgroundImage.Size;
Rectangle rectSrc = new Rectangle(e.X - w2, e.Y - h2, w, h);
Rectangle rectTgt = new Rectangle(e.X - w, e.Y - h, 2 * w, 2 * h);
using (Graphics g = drawPanel1.CreateGraphics()) // start optional
{
g.TranslateTransform(e.X, e.Y);
g.RotateTransform(trb_a.Value);
g.TranslateTransform(-e.X, -e.Y);
drawPanel1.Refresh();
g.DrawRectangle(Pens.White, rectSrc);
}
using (Graphics g = drawPanel2.CreateGraphics())
{ // end optional
using (Bitmap bmp = new Bitmap(sz.Width, sz.Height))
using (Graphics g2 = Graphics.FromImage(bmp))
{
g2.TranslateTransform(e.X, e.Y);
g2.RotateTransform(-trb_a.Value);
g2.TranslateTransform(-e.X, -e.Y);
g2.DrawImage(drawPanel1.BackgroundImage, rectTgt, rectTgt,
GraphicsUnit.Pixel);
drawPanel2.Refresh();
g.DrawImage(bmp, rectSrc, rectSrc, GraphicsUnit.Pixel);
Text = testForYellowBitmap(bmp) ? "!!YELLOW!!" : "";
}
}
}
The first approach would use a similar LockBits method, but with loops inside that go along the rotated sides of the car rectangle, using floats wth the loop variables to calculate the x-coordinates. Those data should be prepared on each change of car size or angle. The code is a little longer but should be a bit faster, too.
The advantage if the second approach is that by using a ClippingRegion on the Graphics object one could check an arbitrary shape while the first method can be easily modified for concave polygons but not for curved shapes.
Here is the adapted version of the checking code for the first version:
public bool testForYellowBitmapTilt(Bitmap bmp, List<int> leftPts,
List<int> rightPts, Point topLeft)
{
Size s1 = bmp.Size;
PixelFormat fmt = new PixelFormat();
fmt = bmp.PixelFormat;
Rectangle rect = new Rectangle(0, 0, s1.Width, s1.Height);
BitmapData bmp1Data = bmp.LockBits(rect, ImageLockMode.ReadOnly, fmt);
byte bpp1 = 4;
if (fmt == PixelFormat.Format24bppRgb) bpp1 = 3;
else if (fmt == PixelFormat.Format32bppArgb) bpp1 = 4;
else return false; // or throw!!
if (leftPts.Count != rightPts.Count) return false; // or throw!!
int size1 = bmp1Data.Stride * bmp1Data.Height;
byte[] data1 = new byte[size1];
System.Runtime.InteropServices.Marshal.Copy(bmp1Data.Scan0, data1, 0, size1);
for (int y = 0; y < (leftPts.Count); y++)
{
for (int x = leftPts[y] + topLeft.X; x < rightPts[y] + topLeft.X; x++)
{
Color c1;
int index1 = (y + topLeft.Y) * bmp1Data.Stride + x * bpp1;
if (index1 > 0)
{
if (bpp1 == 4)
c1 = Color.FromArgb(data1[index1 + 3], data1[index1 + 2],
data1[index1 + 1], data1[index1 + 0]);
else c1 = Color.FromArgb(255, data1[index1 + 2],
data1[index1 + 1], data1[index1 + 0]);
if (c1.R > 220 && c1.G > 220 && c1.B < 200)
{ bmp.UnlockBits(bmp1Data); return true; }
}
}
}
bmp.UnlockBits(bmp1Data);
return false;
}
The left- and rightside coordinates are stored here:
List<int> leftPts = new List<int>();
List<int> rightPts = new List<int>();
Point top = Point.Empty;
void getOuterPoints(List<PointF> corners, out List<int> leftPts,
out List<int> rightPts, out Point top)
{
leftPts = new List<int>();
rightPts = new List<int>();
PointF left = corners.Select(x => x).OrderBy(x => x.X).First();
PointF right = corners.Select(x => x).OrderByDescending(x => x.X).First();
top = Point.Round(corners.Select(x => x).OrderBy(x => x.Y).First());
PointF bottom = corners.Select(x => x).OrderByDescending(x => x.Y).First();
int w1 = -(int)(top.X - left.X);
int w2 = -(int)(left.X - bottom.X );
int h1 = (int)(left.Y - top.Y);
int h2 = (int)(bottom.Y - left.Y);
float d1 = 1f * w1 / h1;
float d2 = 1f * w2 / h2;
for (int y = 0; y < h1; y++) leftPts.Add( (int)(y * d1) );
for (int y = 0; y < h2; y++) leftPts.Add( (int)(y * d2 + w1));
for (int y = 0; y < h2; y++) rightPts.Add( (int)(y * d2));
for (int y = 0; y < h1; y++) rightPts.Add( (int)(y * d1 + w2));
}
You need to feed in the four corners as a List<PointF> in any order; the top can be anything, it will be set in the method. The coodinates are relative to the car, so they don't change when the car moves..

How to draw a circular progressbar pie using GraphicsPath in WinForm?

I want my custom circular progress bar in WinForm. But the result doesnt fit to what I think. How can I draw the shape as the same in this picture?. I uploaded two image to be clear in my problem.
My code to do this:
void Form1_Paint(object sender, PaintEventArgs e)
{
int angle = 120;
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
Rectangle outerRect = new Rectangle(50, 50, 100, 100);
Rectangle innerRect = new Rectangle(70, 70, 60, 60);
int innerRadius = innerRect.Width / 2;
int outerRadius = outerRect.Width / 2;
Point innerCenter = new Point(innerRect.X + innerRadius, innerRect.Y + innerRadius);
Point outerCenter = new Point(outerRect.X + outerRadius, outerRect.Y + outerRadius);
GraphicsPath outerCircle = new GraphicsPath();
outerCircle.AddEllipse(outerRect);
GraphicsPath innerCircle = new GraphicsPath();
innerCircle.AddEllipse(innerRect);
GraphicsPath progPath = new GraphicsPath();
Point p1 = new Point(outerRect.X + outerRadius, outerRect.Y);
Point p2 = new Point(innerRect.X + innerRadius, innerRect.Y);
Point inner = new Point((int)(innerRadius * Math.Cos(angle * Math.PI / 180) + innerCenter.X),
(int)(innerRadius * Math.Sin(angle * Math.PI / 180) + innerCenter.Y));
Point outer = new Point((int)(outerRadius * Math.Cos(angle * Math.PI / 180) + outerCenter.X),
(int)(outerRadius * Math.Sin(angle * Math.PI / 180) + outerCenter.Y));
progPath.AddLine(p1, p2);
progPath.AddArc(innerRect, -90, angle);
progPath.AddLine(inner, outer);
progPath.AddArc(outerRect, angle - 90,-angle);
progPath.Widen(Pens.Black);
e.Graphics.DrawPath(Pens.Black, progPath);
}
You can create a GraphicsPath, then add 2 arcs to the path using AddArc method:
Outer arc from start angle 270 and sweep angle 120 degree.
Inner arc in opposite direction, from start angle 270 + 120 and sweep angle -120 degree
Then close the path using GraphicsPath.CloseFigure.
This way the you will have a thick arc as path.
You can fill the path, using Graphics.FillPath method. And also you can draw the borders using GraphicsPath.DrawPath method.
Result
Code
private void Form1_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
var center = new Point(100, 100);
var innerR = 30;
var thickness = 20;
var startAngle = 270;
var arcLength = 120;
var outerR = innerR + thickness;
var outerRect = new Rectangle
(center.X - outerR, center.Y - outerR, 2 * outerR, 2 * outerR);
var innerRect = new Rectangle
(center.X - innerR, center.Y - innerR, 2 * innerR, 2 * innerR);
using (var p = new GraphicsPath())
{
p.AddArc(outerRect, startAngle, arcLength);
p.AddArc(innerRect, startAngle + arcLength, -arcLength);
p.CloseFigure();
e.Graphics.FillPath(Brushes.Green, p);
e.Graphics.DrawPath(Pens.Black, p);
}
}

How can i draw a trail/trace after a moving line?

My code is drawing a circle then a line from the middle of the circle to the radius size of the circle and the line is moving by 1 angle.
Now i want to make that the line will leave some trail/trace after it like a radar effect.
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
anglecounter += 1;
double x = pictureBox1.Size.Width / 2 + 256 *Math.Cos(anglecounter * Math.PI / 180);
double y = pictureBox1.Size.Height / 2 +256 * Math.Sin(anglecounter * Math.PI / 180);
CloudEnteringAlert.Paint(e.Graphics, factor, distance);
e.Graphics.DrawLine(
new Pen(Color.Red, 2f),
new Point(pictureBox1.Size.Width / 2, pictureBox1.Size.Height/2),
new Point((int)x, (int)y));
e.Graphics.DrawEllipse(
new Pen(Color.Red, 2f),
0, 0, pictureBox1.Size.Width, pictureBox1.Size.Height);
}
How can I do this?
EDIT**
This is what i did now in the top of the form i added:
PointF _pt = new PointF(0F, 0F);
PointF _pt2 = new PointF(1F, 1F);
PointF _pt3 = new PointF(2F, 2F);
Color _lineColor = Color.FromArgb(0, 255, 0);
private double anglecounter1;
Then the paint event is now look like this:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
anglecounter += 1;
anglecounter1 += 0.5;
double x = pictureBox1.Size.Width / 2 + 256 *Math.Cos(anglecounter * Math.PI / 180);
double y = pictureBox1.Size.Height / 2 +256 * Math.Sin(anglecounter * Math.PI / 180);
double x1 = pictureBox1.Size.Width / 2 + 256 * Math.Cos(anglecounter1 * Math.PI / 180);
double y1 = pictureBox1.Size.Height / 2 + 256 * Math.Sin(anglecounter1 * Math.PI / 180);
CloudEnteringAlert.Paint(e.Graphics, factor, distance);
e.Graphics.DrawLine(
new Pen(Color.Red, 2f),
new Point(pictureBox1.Size.Width / 2, pictureBox1.Size.Height/2),
new Point((int)x, (int)y));
e.Graphics.DrawEllipse(
new Pen(Color.Red, 2f),
0, 0, pictureBox1.Size.Width, pictureBox1.Size.Height);
// create the fade path and gradient
GraphicsPath gp = new GraphicsPath(FillMode.Winding);
gp.AddLine(new PointF((float)(pictureBox1.Size.Width / 2), (float)(pictureBox1.Size.Height / 2)),new PointF( (float)x1,(float)y1));
gp.AddCurve(new PointF[] { _pt2, _pt3, _pt });
gp.AddLine(new PointF((float)x, (float)y), new PointF((float)(pictureBox1.Size.Width / 2), (float)(pictureBox1.Size.Height / 2)));
PathGradientBrush pgb = new PathGradientBrush(gp);
pgb.CenterPoint = new PointF((float)x1, (float)y1);
pgb.CenterColor = Color.FromArgb(128, _lineColor);
pgb.SurroundColors = new Color[] { Color.Empty };
// draw the fade path
e.Graphics.FillPath(pgb, gp);
}
But if im not wrong in this case the trail/trace is slower then the line is getting faster then it. Also the trail/trace is also behind the line but also in front of the line . Im not sure but thats what i see. So what is wrong ?
Another option is to not erase the image each time. Instead, draw a semi-transparent circle over the previous image:
// Initialize some dimensions
int x = pictureBox1.Bounds.X;
int y = pictureBox1.Bounds.Y;
int w = Math.Min(pictureBox1.Bounds.Width, pictureBox1.Bounds.Height);
int h = w; // Force square
int centerX = w / 2;
int centerY = h / 2;
float radius = w - centerX;
Graphics g = pictureBox1.CreateGraphics();
// First time draw a solid background then
// each successive time cover with semi-transparent background
Brush backGround = firstTime ? new SolidBrush(Color.FromArgb(255, 0, 0, 0)) : new SolidBrush(Color.FromArgb(10, 0, 0, 0));
firstTime = false;
g.FillEllipse(backGround, 0, 0, w, h);
float lineX = (float)(centerX + (radius * Math.Sin(anglecounter * (Math.PI / 180))));
float lineY = (float)(centerX + (radius * Math.Cos(anglecounter * (Math.PI / 180))));
anglecounter -= 1;
g.DrawLine(new Pen(Color.Green, 3), centerX, centerY, lineX, lineY);
g.DrawArc(new Pen(Color.Red, 4), new Rectangle(0, 0, w - 1, h - 1), 0, 360);
Produces this result:

Player Coordinates into 2D Screen Coordinates

My project's main goal is to project a player character from a 3D environment to a 2D screen. I already found some useful math online, but in the last four days I couldn't make it work. Everytime I move the character it appears in random places and/or goes off screen. Sometimes it follows horizontally, but usually never vertically.
My question is pretty simple: What am I doing wrong?
// A few numbers to play with:
// Case #1: myPos: 1104.031, 3505.031, -91.9875; myMousePos: 0, 180; myRotation: 153, 153, 25; playerPos: 1072, 3504, -91.9687 (Player Middle of Screen, Standing Behind Player)
// Case #2: myPos: 511.7656, 3549.25, -28.02344; myMousePos: 0, 347.5854; myRotation: 44, 2, 22; playerPos: 1632, 3232, -91.96875 (Player Middle of Screen, I stand higher and 1166 away)
// Case #3: myPos: 1105.523, 2898.336, -11.96875; myMousePos: 0, 58.67249; myRotation: 232, 184, 159; playerPos 1632, 3232, -91.96875 (Player Right, Lower Of Screen)
Vect3d viewAngles;
Vect3d vForward, vRight, vUpward;
float ScreenX, ScreenY;
float[] fov;
bool 3dWorldTo2dScreen(Vect3d playerpos, Vect3d mypos, PlayerData myself)
{
fov = new float[2];
viewAngles = Vect3d();
Vect3d vLocal, vTransForm;
vTransForm = new Vect3d();
vForward = new Vect3d();
vRight = new Vect3d();
vUpward = new Vect3d();
fov[0] = myself.MouseX; // Sky: -89, Ground: 89, Middle: 0
fov[1] = myself.MouseY; // 360 To 0
viewAngles.x = myself.Rotation.x;
viewAngles.y = myself.Rotation.y;
viewAngles.z = myself.Rotation.z;
int screenCenterX = 320; // 640
int screenCenterY = 240; // 480
AngleVectors();
vLocal = SubVectorDist(playerpos, mypos);
vTransForm.x = vLocal.dotproduct(vRight);
vTransForm.y = vLocal.dotproduct(vUpward);
vTransForm.z = vLocal.dotproduct(vForward);
if (vTransForm.z < 0.01)
return false;
ScreenX = screenCenterX + (screenCenterX / vTransForm.z * (1 / fov[0])) * vTransForm.x;
ScreenY = screenCenterY - (screenCenterY / vTransForm.z * (1 / fov[1])) * vTransForm.y;
return true;
}
Vect3d SubVectorDist(Vect3d playerFrom, Vect3d playerTo)
{
return new Vect3d(playerFrom.x - playerTo.x, playerFrom.y - playerTo.y, playerFrom.z - playerTo.z);
}
private void AngleVectors()
{
float angle;
float sr, sp, sy, cr, cp, cy,
cpi = (3.141f * 2 / 360);
angle = viewAngles.y * cpi;
sy = (float)Math.Sin(angle);
cy = (float)Math.Cos(angle);
angle = viewAngles.x * cpi;
sp = (float)Math.Sin(angle);
cp = (float)Math.Cos(angle);
angle = viewAngles.z * cpi;
sr = (float)Math.Sin(angle);
cr = (float)Math.Cos(angle);
vForward.x = cp * cy;
vForward.y = cp * sy;
vForward.z = -sp;
vRight.x = (-1 * sr * sp * cy + -1 * cr * -sy);
vRight.y = (-1 * sr * sp * sy + -1 * cr * cy);
vRight.z = -1 * sr * cp;
vUpward.x = (cr * sp * cy + -sr * -sy);
vUpward.y = (cr * sp * sy + -sr * cy);
vUpward.z = cr * cp;
}

Categories