In FireMonkey, it is simple to draw a bitmap to a source rectangle:
Canvas.DrawBitmap(FBitmap, ImageSrcRect, ImageDstRect, 1);
And I'm doing this on a TPaintBox
's canvas. I would instead like to draw the bitmap rotated (and scaled, since the destination size may not be the same as the source size.)
Specifically:
as in this image:
One the left is what I can currently do; on the right is what I would like to do.
What's the best way to do it?
In order to keep existing code simple (eg, drawing to a destination rectangle, thereby scaling the result) I've been trying to add a rotation matrix to the canvas's matrix before calling the existing DrawBitmap code. For example,
OldMatrix := Canvas.Matrix; // Original, to restore
W := PointB.X - PointA.X;
H := PointA.Y - PointB.Y;
RotationMatrix := TMatrix.CreateRotation(-ArcTan2(H, W));
Canvas.SetMatrix(OldMatrix * RotationMatrix);
Canvas.DrawBitmap(FImage, ImageSrcRect, ImageDstRect, 1);
Canvas.SetMatrix(OldMatrix);
and a couple of variations multiplying with the existing matrix, creating an entirely new matrix with both translation and rotation, etc. All these partially work: the rotation angle is correct, but I'm having a lot of trouble getting the position to stay consistent - for example, to rotate around the center point (and this isn't even getting to rotating the top of the bitmap around the point, not rotating around the center.) I've found that the rotated image is offset fine in the bottom right quadrant, but in the other three is offset / translated incorrectly, such as far too far left, or clipped to the leftmost or topmost X or Y position of the two points. I do not know why this is, and it's at this point I'm asking SO for help.
TPaintBox
, arbitrarily placed. The paint box may itself be on a TScaledLayout
.As far as I can understand the main problem is to find the coordinates of the corner of the picture in the new rotated system of coordinates. This can be solved in the following way:
procedure DrawRotatedBitmap(const Canvas : TCanvas; const Bitmap : TBitmap;
const PointA, PointB : TPointF; const Offset : TPointF; const Scale : Single);
var
OldMatrix, TranslationAlongLineMatrix, RotationMatrix, TranslationMatrix,
ScaleMatrix, FinalMatrix: TMatrix;
W, H : Single;
SrcRect, DestRect: TRectF;
Corner: TPointF;
LineLength : Single;
LineAngleDeg : Integer;
begin
OldMatrix := Canvas.Matrix; // Original, to restore
try
{$ifdef DRAW_HELPERS}
Canvas.Fill.Color := TAlphaColorRec.Black;
Canvas.DrawLine(PointA, PointB, 0.5);
{$endif}
W := PointB.X - PointA.X;
H := PointA.Y - PointB.Y;
LineLength := abs(PointA.Distance(PointB));
// Looking for the middle of the task line
// and the coordinates of the image left upper angle
// solving the proportion width/linelength=xo/0.5requireddimensions
Corner := TPointF.Create((PointB.X + PointA.X) / 2, (PointA.Y + PointB.Y) / 2);// Middle
{$ifdef DRAW_HELPERS}
Canvas.Stroke.Color := TAlphaColorRec.Red;
Canvas.DrawEllipse(TRectF.Create(Corner,2,2),1);
{$endif}
Corner.X := Corner.X - Bitmap.Width / 2 * W / LineLength;
Corner.Y := Corner.Y + Bitmap.Width / 2 * H / LineLength;
{$ifdef DRAW_HELPERS}
Canvas.Stroke.Color := TAlphaColorRec.Green;
Canvas.DrawEllipse(TRectF.Create(Corner,2,2),1);
{$endif}
// Account for scale (if the FMX control is scaled / zoomed); translation
// (the control may not be located at (0, 0) in its parent form, so its canvas
// is offset) and rotation
ScaleMatrix := TMatrix.CreateScaling(Scale, Scale);
TranslationMatrix := TMatrix.CreateTranslation(Offset.X, Offset.Y);
RotationMatrix := TMatrix.CreateRotation(-ArcTan2(H, W));
TranslationAlongLineMatrix := TMatrix.CreateTranslation(Corner.X, Corner.Y);
FinalMatrix := ((RotationMatrix * ScaleMatrix) * TranslationMatrix) * TranslationAlongLineMatrix;
// If in the top left or top right quadrants, the image will appear
// upside down. So, rotate the image 180 degrees
// This is useful when the image contains text, ie is an annotation or similar,
// or needs to always appear "under" the line
LineAngleDeg := Round(RadToDeg(-Arctan2(H, W)));
case LineAngleDeg of
-180..-90,
90..180 : FinalMatrix := TMatrix.CreateRotation(DegToRad(180)) * TMatrix.CreateTranslation(Bitmap.Width, 0) * FinalMatrix;
end;
Canvas.SetMatrix(FinalMatrix);
// And finally draw the bitmap
DestRect := TRectF.Create(PointF(0, 0), Bitmap.Width, Bitmap.Height);
SrcRect := TRectF.Create(0, 0, Bitmap.Width, Bitmap.Height);
{$ifdef DRAW_HELPERS}
Canvas.DrawBitmap(Bitmap, SrcRect, DestRect, 0.5);
{$else}
Canvas.DrawBitmap(Bitmap, SrcRect, DestRect, 1);
{$endif}
finally
// Restore the original matrix
Canvas.SetMatrix(OldMatrix);
end;
end;
There are ifdef
-ed paintings of lines and points that might help you as well - these draw the line and some helpful points (line center and image top-left corner), which are useful for debugging.
Edit by DavidM: In addition, there are also translation and scaling matrices. A paintbox draws on the parent form's canvas (ultimately) but may not be located at (0, 0), so the offset position of the destination canvas needs to be accounted for. Also, a control can have scaling, so that too needs to be built into the final rotated matrix.
This code is heavily edited and works regardless of the orientation / quadrant the line angle is in. That is, it should work when the line is completely horizontal or vertical, as well as when in quadrants other then the bottom-right one.
One interesting tweak is recognising that the bitmap in the example contains text. When the line is in the top-left or top-right quadrants - that is, going up and then either left or right from its origin - the bitmap appears upside down to the human eye. The tweak recognises that and rotates the bitmap so that the "top" of the bitmap always faces the line, and the "bottom" of the bitmap generally points downwards, making the image appear the right way up. You can remove this tweak if you don't need an image that represents something recognisable (eg a symbol, text, label, etc.)
With both different angles and scaling.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With