How to draw an arc on WritableBitmap + WritablebitmapEx - c#

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;

Related

How to decrease and increase values as a curve

I am trying to do a digging tool for my game, I have x and y coordinates of point A and B, what I want to do is create a curve between these points, nothing graphical I just need loop through the coordinates (float x, float y).
I am not good at explaining so here is a visual example;
The first image is what's happen if I just use a for loop to decrease the y value until middle and then increase it from the middle to end.
//Very specific code for my example
//I wrote it just for this example so I am not sure if it works
float y;
float x;
public void Example(float startX, float endX, float startY, float endY, float depth)
{
y = startY;
x = startX;
float changeAmountOfY = depth / (endX - startX);
for (int i = (int)startX; i < (startX + endX) / 2; i++)
{
x++;
y -= changeAmountOfY;
}
for (int i = (int)(startX + endX) / 2; i < endX; i++)
{
x++;
y += changeAmountOfY;
}
}
public void ChangeCoordinates()
{
Example(100f, 200f, 100f, 100f, 50f);
}
The second image is what I need.
I am developing the game on unity and I am using Vector2 for the coordinates but it is not important.
Pure C# or even C++ is welcome.
It is also fine if someone can just explain the math behind what I am trying to do.
Maybe this can help:
// Calculate radius
int radius = (B.X - A.X) / 2;
// Calculate middle
int middle_x = A.X + radius;
int middle_y = A.Y;
// or
int middle_y = (A.Y + B.Y) / 2;
// Coordinates for a semicircle
// 0 to 180 degree
for (int i = 0; i <= 180; i++)
{
double x_coordinate = middle_x + radius * Math.Cos(i * Math.PI / 180);
// Opened to bottom
double y_coordinate = middle_y + radius * Math.Sin(i * Math.PI / 180);
// or opened to top
double y_coordinate = middle_y - radius * Math.Sin(i * Math.PI / 180);
}
Take a look at unit circle.

How to draw an audio waveform to a bitmap

I am attempting to extract the audio content of a wav file and export the resultant waveform as an image (bmp/jpg/png).
So I have found the following code which draws a sine wave and works as expected:
string filename = #"C:\0\test.bmp";
int width = 640;
int height = 480;
Bitmap b = new Bitmap(width, height);
for (int i = 0; i < width; i++)
{
int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
b.SetPixel(i, y, Color.Black);
}
b.Save(filename);
This works completely as expected, what I would like to do is replace
int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
with something like
int y = converted and scaled float from monoWaveFileFloatValues
So how would I best go about doing this in the simplest manner possible?
I have 2 basic issues I need to deal with (i think)
convert float to int in a way which does not loose information, this is due to SetPixel(i, y, Color.Black); where x & y are both int
sample skipping on the x axis so the waveform fits into the defined space audio length / image width give the number of samples to average out intensity over which would be represented by a single pixel
The other options is find another method of plotting the waveform which does not rely on the method noted above. Using a chart might be a good method, but I would like to be able to render the image directly if possible
This is all to be run from a console application and I have the audio data (minus the header) already in a float array.
UPDATE 1
The following code enabled me to draw the required output using System.Windows.Forms.DataVisualization.Charting but it took about 30 seconds to process 27776 samples and whilst it does do what I need, it is far too slow to be useful. So I am still looking towards a solution which will draw the bitmap directly.
System.Windows.Forms.DataVisualization.Charting.Chart chart = new System.Windows.Forms.DataVisualization.Charting.Chart();
chart.Size = new System.Drawing.Size(640, 320);
chart.ChartAreas.Add("ChartArea1");
chart.Legends.Add("legend1");
// Plot {sin(x), 0, 2pi}
chart.Series.Add("sin");
chart.Series["sin"].LegendText = args[0];
chart.Series["sin"].ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Spline;
//for (double x = 0; x < 2 * Math.PI; x += 0.01)
for (int x = 0; x < audioDataLength; x ++)
{
//chart.Series["sin"].Points.AddXY(x, Math.Sin(x));
chart.Series["sin"].Points.AddXY(x, leftChannel[x]);
}
// Save sin_0_2pi.png image file
chart.SaveImage(#"c:\tmp\example.png", System.Drawing.Imaging.ImageFormat.Png);
Output shown below:
So I managed to figure it out using a code sample found here, though I made some minor changes to the way I interact with it.
public static Bitmap DrawNormalizedAudio(List<float> data, Color foreColor, Color backColor, Size imageSize, string imageFilename)
{
Bitmap bmp = new Bitmap(imageSize.Width, imageSize.Height);
int BORDER_WIDTH = 0;
float width = bmp.Width - (2 * BORDER_WIDTH);
float height = bmp.Height - (2 * BORDER_WIDTH);
using (Graphics g = Graphics.FromImage(bmp))
{
g.Clear(backColor);
Pen pen = new Pen(foreColor);
float size = data.Count;
for (float iPixel = 0; iPixel < width; iPixel += 1)
{
// determine start and end points within WAV
int start = (int)(iPixel * (size / width));
int end = (int)((iPixel + 1) * (size / width));
if (end > data.Count)
end = data.Count;
float posAvg, negAvg;
averages(data, start, end, out posAvg, out negAvg);
float yMax = BORDER_WIDTH + height - ((posAvg + 1) * .5f * height);
float yMin = BORDER_WIDTH + height - ((negAvg + 1) * .5f * height);
g.DrawLine(pen, iPixel + BORDER_WIDTH, yMax, iPixel + BORDER_WIDTH, yMin);
}
}
bmp.Save(imageFilename);
bmp.Dispose();
return null;
}
private static void averages(List<float> data, int startIndex, int endIndex, out float posAvg, out float negAvg)
{
posAvg = 0.0f;
negAvg = 0.0f;
int posCount = 0, negCount = 0;
for (int i = startIndex; i < endIndex; i++)
{
if (data[i] > 0)
{
posCount++;
posAvg += data[i];
}
else
{
negCount++;
negAvg += data[i];
}
}
if (posCount > 0)
posAvg /= posCount;
if (negCount > 0)
negAvg /= negCount;
}
In order to get it working I had to do a couple of things prior to calling the method DrawNormalizedAudio you can see below what I needed to do:
Size imageSize = new Size();
imageSize.Width = 1000;
imageSize.Height = 500;
List<float> lst = leftChannel.OfType<float>().ToList(); //change float array to float list - see link below
DrawNormalizedAudio(lst, Color.Red, Color.White, imageSize, #"c:\tmp\example2.png");
* change float array to float list
The result of this is as follows, a waveform representation of a hand clap wav sample:
I am quite sure there needs to be some updates/revisions to the code, but it's a start and hopefully this will assist someone else who is trying to do the same thing I was.
If you can see any improvements that can be made, let me know.
UPDATES
NaN issue mentioned in the comments now resolved and code above updated.
Waveform Image updated to represent output fixed by removal of NaN values as noted in point 1.
UPDATE 1
Average level (not RMS) was determined by summing the max level for each sample point and dividing by the total number of samples. Examples of this can be seen below:
Silent Wav File:
Hand Clap Wav File:
Brownian, Pink & White Noise Wav File:
Here is a variation you may want to study. It scales the Graphics object so it can use the float data directly.
Note how I translate (i.e. move) the drawing area twice so I can do the drawing more conveniently!
It also uses the DrawLines method for drawing. The benefit in addition to speed is that the lines may be semi-transparent or thicker than one pixel without getting artifacts at the joints. You can see the center line shine through.
To do this I convert the float data to a List<PointF> using a little Linq magick.
I also make sure to put all GDI+ objects I create in using clause so they will get disposed of properly.
...
using System.Windows.Forms;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
..
..
class Program
{
static void Main(string[] args)
{
float[] data = initData(10000);
Size imgSize = new Size(1000, 400);
Bitmap bmp = drawGraph(data, imgSize , Color.Green, Color.Black);
bmp.Save("D:\\wave.png", ImageFormat.Png);
}
static float[] initData(int count)
{
float[] data = new float[count];
for (int i = 0; i < count; i++ )
{
data[i] = (float) ((Math.Sin(i / 12f) * 880 + Math.Sin(i / 15f) * 440
+ Math.Sin(i / 66) * 110) / Math.Pow( (i+1), 0.33f));
}
return data;
}
static Bitmap drawGraph(float[] data, Size size, Color ForeColor, Color BackColor)
{
Bitmap bmp = new System.Drawing.Bitmap(size.Width, size.Height,
PixelFormat.Format32bppArgb);
Padding borders = new Padding(20, 20, 10, 50);
Rectangle plotArea = new Rectangle(borders.Left, borders.Top,
size.Width - borders.Left - borders.Right,
size.Height - borders.Top - borders.Bottom);
using (Graphics g = Graphics.FromImage(bmp))
using (Pen pen = new Pen(Color.FromArgb(224, ForeColor),1.75f))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Silver);
using (SolidBrush brush = new SolidBrush(BackColor))
g.FillRectangle(brush, plotArea);
g.DrawRectangle(Pens.LightGoldenrodYellow, plotArea);
g.TranslateTransform(plotArea.Left, plotArea.Top);
g.DrawLine(Pens.White, 0, plotArea.Height / 2,
plotArea.Width, plotArea.Height / 2);
float dataHeight = Math.Max( data.Max(), - data.Min()) * 2;
float yScale = 1f * plotArea.Height / dataHeight;
float xScale = 1f * plotArea.Width / data.Length;
g.ScaleTransform(xScale, yScale);
g.TranslateTransform(0, dataHeight / 2);
var points = data.ToList().Select((y, x) => new { x, y })
.Select(p => new PointF(p.x, p.y)).ToList();
g.DrawLines(pen, points.ToArray());
g.ResetTransform();
g.DrawString(data.Length.ToString("###,###,###,##0") + " points plotted.",
new Font("Consolas", 14f), Brushes.Black,
plotArea.Left, plotArea.Bottom + 2f);
}
return bmp;
}
}

Adjusting the spread of a vignette effect

I'm using the following code to produce a vignette effect on an image. As you can see below it works quite well. I'd like to be able to adjust the inner spread of the vignette (i.e make the middle brighter and shorten the gradient) however the maths have got the better of me. Could anyone please give me some pointers with an explanation?
protected override void Apply(ImageBase target,
ImageBase source,
Rectangle targetRectangle,
Rectangle sourceRectangle,
int startY, int endY)
{
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
Color color = this.Color;
Vector2 centre = Rectangle.Center(targetRectangle);
float rX = this.RadiusX > 0 ? this.RadiusX : targetRectangle.Width / 2f;
float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f;
float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY);
Parallel.For(
startY,
endY,
y =>
{
for (int x = startX; x < endX; x++)
{
float distance = Vector2.Distance(centre, new Vector2(x, y));
Color sourceColor = target[x, y];
target[x, y] = Color.Lerp(sourceColor,
color, .9f * distance / maxDistance);
}
});
}
Original Image
Vignette Effect

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 rotate image x degrees in c#?

I have done some searching and i can not find any function thats doing what i whant it todo.
I have a image file of a scanned document with text, but the document is some degrees rotated so i whant to rotate it so the text being inline with each other.
In a perfect world its should be a function doing this automaticly but i can not find anything and what i understand to get it to work automaticly its needed to be some analyze of the image and i think its to big thing todo.
But then i have done a tool to rotate the image on a website manually, but now i need a function to save the rotation to the image file.
This seems to be some differents methods for but no one i tested doing what i whant.
The function i have finded that works almost like i whant is:
public static Bitmap RotateImg(Bitmap bmp, float angle, Color bkColor) {
int w = bmp.Width;
int h = bmp.Height;
PixelFormat pf = default(PixelFormat);
if (bkColor == Color.Transparent)
{
pf = PixelFormat.Format32bppArgb;
}
else
{
pf = bmp.PixelFormat;
}
Bitmap tempImg = new Bitmap(w, h, pf);
Graphics g = Graphics.FromImage(tempImg);
g.Clear(bkColor);
g.DrawImageUnscaled(bmp, 1, 1);
g.Dispose();
GraphicsPath path = new GraphicsPath();
path.AddRectangle(new RectangleF(0f, 0f, w, h));
Matrix mtrx = new Matrix();
//Using System.Drawing.Drawing2D.Matrix class
mtrx.Rotate(angle);
RectangleF rct = path.GetBounds(mtrx);
Bitmap newImg = new Bitmap(Convert.ToInt32(rct.Width), Convert.ToInt32(rct.Height), pf);
g = Graphics.FromImage(newImg);
g.Clear(bkColor);
g.TranslateTransform(-rct.X, -rct.Y);
g.RotateTransform(angle);
g.InterpolationMode = InterpolationMode.HighQualityBilinear;
g.DrawImageUnscaled(tempImg, 0, 0);
g.Dispose();
tempImg.Dispose();
return newImg; }
But this do not change the height and width of the image file so the image file is in the same size but the image "object" has been scaled and rotated.
Any idea how i can do this good?
Answer
I find the solution that worked with my image that has a resolution on 300 at a old answer here.
If I've understood your question correctly, you essentially want to work out the new size of an image once rotated, and how to position the rotated image in it's new bitmap.
The diagram hopefully helps make clear the solution. Here is a bit of pseudo code:
sinVal = abs(sin(angle))
cosVal = abs(cos(angle))
newImgWidth = sinVal * oldImgHeight + cosVal * oldImgWidth
newImgHeight = sinVal * oldImgWidth + cosVal * oldImgHeight
originX = 0
originY = sinVal * oldImgWidth
You want to make the new image from the newImgWidth and newImgHeight, and then perform a rotation around the origin (originX, originY) and then render the image to this point. This will fall over if the angle (in degrees) isn't between -90 and 0 degrees (depicted). If it is between 0 and 90 degrees, then you just change the origin:
originX = sinVal * oldImgHeight
originY = 0
If it is in the range 90 degrees to 270 degrees (-90 degrees) then it is a little tricker (see example code below).
Your code re-written (briefly tested) - it is slightly dodgy but seems to work:
public static Bitmap RotateImg(Bitmap bmp, float angle, Color bkColor)
{
angle = angle % 360;
if (angle > 180)
angle -= 360;
System.Drawing.Imaging.PixelFormat pf = default(System.Drawing.Imaging.PixelFormat);
if (bkColor == Color.Transparent)
{
pf = System.Drawing.Imaging.PixelFormat.Format32bppArgb;
}
else
{
pf = bmp.PixelFormat;
}
float sin = (float)Math.Abs(Math.Sin(angle * Math.PI / 180.0)); // this function takes radians
float cos = (float)Math.Abs(Math.Cos(angle * Math.PI / 180.0)); // this one too
float newImgWidth = sin * bmp.Height + cos * bmp.Width;
float newImgHeight = sin * bmp.Width + cos * bmp.Height;
float originX = 0f;
float originY = 0f;
if (angle > 0)
{
if (angle <= 90)
originX = sin * bmp.Height;
else
{
originX = newImgWidth;
originY = newImgHeight - sin * bmp.Width;
}
}
else
{
if (angle >= -90)
originY = sin * bmp.Width;
else
{
originX = newImgWidth - sin * bmp.Height;
originY = newImgHeight;
}
}
Bitmap newImg = new Bitmap((int)newImgWidth, (int)newImgHeight, pf);
Graphics g = Graphics.FromImage(newImg);
g.Clear(bkColor);
g.TranslateTransform(originX, originY); // offset the origin to our calculated values
g.RotateTransform(angle); // set up rotate
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
g.DrawImageUnscaled(bmp, 0, 0); // draw the image at 0, 0
g.Dispose();
return newImg;
}
Note the Degrees to Radians Conversion (180 Degrees == Pi Radians) for the trig functions
Edit: big issue was negative sin values, and me getting width/height confused when calculating origin x/y - this should work fine now (tested)
Edit: modified code to handle any angle
This is strictly a comment to the nice answer by VisualMelon above, But I'm not allowed to add comments...
There are two tiny bugs in the code
a) The first check after the modulus should either be split into two, or changed to e.g.
if (180<Math.Abs(angle)) angle -= 360*Math.Sign(angle);
Otherwise angles between -360 and -180 will fail, as only +180 to +360 were handled
b) Just after the newImg assignment, a resolution assignment is missing, e.g.
newImg.SetResolution(bmp.HorizontalResolution, bmp.VerticalResolution);
If omitted the image will be scaled if the source is not 96 dpi.
....And splitting sticks, the intermediate calculations of dimensions and offsets ought to be kept in double, and only reduced to float last

Categories

Resources