5

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:

  • I have two points
  • The image should be placed below the centerpoint between these two points
  • The image should rotate to follow the angle between the two points

as in this image:

Desired rotation

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?

What I've tried

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.

Details

  • Delphi 10 Seattle
  • FireMonkey (on Windows)
  • Target is the canvas of a TPaintBox, arbitrarily placed. The paint box may itself be on a TScaledLayout.
  • The goal is to draw a bitmap to a rotated target rectangle on the paintbox.
David
  • 13,360
  • 7
  • 66
  • 130

1 Answers1

5

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.)

Illustrations

With both different angles and scaling.

enter image description here enter image description here

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
asd-tm
  • 3,381
  • 2
  • 24
  • 41
  • Thanks. Could you explain some things about the code, please? Eg `RequiredDimention` - why is that hardcoded, couldn't you get it from `Image1.Width`? What about the hardcoded `110`s when creating `DestRect`? Currently using this code, I get no image onscreen at all, even trying to mimic what I guess are your setup conditions. – David Jan 11 '16 at 20:12
  • Perhaps you could change the method signature, please, to something like `DrawBitmap(const Canvas : TCanvas; const Bitmap : TBitmap; const PointA, PointB : TPointF)` - that would remove constants and other assumptions? – David Jan 11 '16 at 20:13
  • @DavidM `RequiredDimention` is hardcoded to simplify the code. You can use a variable and assing a value to it. You can use Image1.Width for this purpose. But you were talking about scaling, weren't you? So the value might be different. The code works otherwise how could I get the screenshots :-) Just set any other values instead of 110 and `RequiredDimention` should you decide to scale your image in a different way. The two different screenshots were done with different values of these parameters. – asd-tm Jan 11 '16 at 20:29
  • @DavidM The first 110 refers to the new width of the picture, the second - to the new height. The `RequiredDimention` is used to find the proper point for the left upper corner of the picture. It sould be equal to the first 110. You can set these parameters to any other values. For example assign 50 to `RequiredDimention` and call DrawImage(...50, 40...) etc. Try to reread the code and understand it. It is not difficult actually. It is based on similar triangles principle (a1/a2=b1/b2=c1/c2). The triangles are similar because their angles are equal. – asd-tm Jan 11 '16 at 20:31
  • Okay... I'm still unclear on "RequiredDimention is used to find the proper point for the left upper corner of the picture" - why isn't that constant a point, then? Is this to offset in the canvas, and if so, why, since it should be centered between the two points A and B? Any chance you could change the method signature so it derives all information from the parameters, perhaps, please? – David Jan 11 '16 at 20:38
  • @DavidM Uncommenting the lines might help you as well. The supplementary dots and lines will be drawn illustrating the stages of the solution. – asd-tm Jan 11 '16 at 20:38
  • @DavidM because it is width of the newly drawn image. Half of this value will be the x offset Your question requires the picure to be drawn 1/2 to the left and 1/2 to the right from the middle of the line. We find the middle point coordinates (betweeen A and B) and then offset it to the left by 1/2 of the required width of the image (`RequiredDimention`). – asd-tm Jan 11 '16 at 20:45
  • Thanks. I've modified the code by removing constants and passing in the items as parameters - eg the canvas to paint on, the bitmap to draw, etc - and adding a couple of extra matrices. The location is especially important since a control is probably not located at (0, 0) so it needs a translation (one is already in the control's canvas by default.) Ditto scaling, since controls can be scaled. Define `DRAW_HELPERS` to have the line and points drawn, too. – David Jan 12 '16 at 17:47
  • And with the edited code I just put in, I've accepted the answer. Thankyou! – David Jan 12 '16 at 17:51
  • This seems buggy when drawing with angles in any other quadrant. Eg from (0,0) to (120,120), the line draws as though it was (0, 120) to (120, 0). When the line is completely vertical or horizontal, it fails to draw completely (NANs in some variables.) Could you examine to see if its logic is correct, please? I temporarily removed the 'correct answer' flag while verifying this. – David Jan 13 '16 at 18:03
  • @DavidM It is not a bug. These variants vere not coded. My job was to give you the idea and solve the current task shown in the picture. You have the idea of how to count the necessary points coordinates. If you have understood the code it will be not a problem to use this design to calculate the positions for the other angles. I give you the hint. Use `CoefVirt` to know the angle of the line – asd-tm Jan 13 '16 at 18:34
  • I see. Usually we try here to give complete answers - an answer that works for 1/4 of solutions isn't much. Leaving something unsolved is usually only done if the answerer can't solve it, not for any other motivation. However, thanks for the "hints". If I figure out a generally applicable solution I'll post an answer. – David Jan 13 '16 at 19:18
  • @DavidM You are mistaken. The 4/5 of answers here are the the hints and peaces of advice with or even without 15-20 lines of code. Nobody posts here a hundred line of code and does the whole work for you. I think I have done enough. My answer gives you the 1. Proper idea and 2. Working code for your illustration in your question. If you have questions of how my code works you are welcome to chat, I shall do my best to explain. – asd-tm Jan 13 '16 at 19:44
  • "The 4/5 of answers here are the the hints and peaces of advice with or even without 15-20 lines of code. Nobody posts here a hundred line of code and does the whole work for you." I disagree. A good answer is complete, verifiable, and solves the problem. Looks at, eg: http://stackoverflow.com/questions/12350624/ and http://stackoverflow.com/questions/1721700/ and http://stackoverflow.com/questions/16384520/. Also http://meta.stackexchange.com/questions/7656/ - it's clear code that _solves the problem_ is key, plus _clear full explanation_. Don't leave the problem unsolved if you can avoid it. – David Jan 13 '16 at 20:19
  • Or perhaps another response to "The 4/5 of answers here are the the hints and peaces of advice" is, just because you've seen others give half-done, half-made, not-completely-useful answers doesn't make them a good example to follow. Instead, comment and even downvote bad-quality answers, and encourage good answers when you see them :) – David Jan 13 '16 at 20:20
  • I've made another edit. This one solves the problem for all quadrants, ie does not fail when vertical, horizontal, or in a quadrant other than the bottom-right. It also removes a bunch of the math and now unused vars, so simplifies the method. It also has a nice tweak that keeps the bitmap "upright". This is in addition to the previous changes, see those comments. I think this is a high quality answer now - I hope you see the evolution of the answer & code and follow why. Thankyou for the original, which was very helpful, and I look forward to seeing your other answers on SO in future too :) – David Jan 13 '16 at 20:29
  • @DavidM I have just finished coding the full answer for your question and was going to post it and even have copied it to the buffer. But now I can see that you have already edited the answer. I shall not post my edit then. Thank you too for your cooperation. See you! – asd-tm Jan 13 '16 at 20:48
  • You could add it as a second code snippet - the more the merrier :) – David Jan 13 '16 at 21:05
  • @DavidM It is not worth doing. Your code is much better then mine due to `TranslationMatrix`. I did not know it and did the coordinates' calculations manually for 5 kinds of lines ("horizontal", "vertical", "/" and two different "\" [crossing the y axis at y<0 and >=0]). Cheers! – asd-tm Jan 13 '16 at 21:12