Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you draw an image to a rotated rectangle in FireMonkey?

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.
like image 315
David Avatar asked Jan 03 '16 18:01

David


1 Answers

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

like image 109
asd-tm Avatar answered Oct 19 '22 18:10

asd-tm