"It is not the language that makes programs appear simple. It is the programmer that make the language appear simple!"
"Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
Related Stack Overflow questions:
- How do I rotate an image around its center using PyGame?
- pygame doesn't rotate surface
- How can you rotate an image around an off center pivot in PyGame
- How to rotate an image around its center while its scale is getting larger(in Pygame)
- How to set the pivot point (center of rotation) for pygame.transform.rotate()?
- Rotating and scaling an image around a pivot, while scaling width and height separately in Pygame
- how to draw line in pygame from player
- How to create sprites around a location?
Short answer:
When you use pygame.transform.rotate
the size of the new rotated image is increased compared to the size of the original image. You must make sure that the rotated image is placed so that its center remains in the center of the non-rotated image. To do this, get the rectangle of the original image and set the position. Get the rectangle of the rotated image and set the center position through the center of the original rectangle.
Returns a tuple from the function red_center
, with the rotated image and the bounding rectangle of the rotated image:
def rot_center(image, angle, x, y):
rotated_image = pygame.transform.rotate(image, angle)
new_rect = rotated_image.get_rect(center = image.get_rect(center = (x, y)).center)
return rotated_image, new_rect
Or write a function which rotates and .blit
the image:
def blitRotateCenter(surf, image, topleft, angle):
rotated_image = pygame.transform.rotate(image, angle)
new_rect = rotated_image.get_rect(center = image.get_rect(topleft = topleft).center)
surf.blit(rotated_image, new_rect.topleft)
Long answer:
For the following examples and explanation I'll use a simple image generated by a rendered text:
font = pygame.font.SysFont('Times New Roman', 50)
text = font.render('image', False, (255, 255, 0))
image = pygame.Surface((text.get_width()+1, text.get_height()+1))
pygame.draw.rect(image, (0, 0, 255), (1, 1, *text.get_size()))
image.blit(text, (1, 1))
An image (pygame.Surface
) can be rotated by pygame.transform.rotate
.
If that is done progressively in a loop, then the image gets distorted and rapidly increases:
while not done:
# [...]
image = pygame.transform.rotate(image, 1)
screen.blit(image, pos)
pygame.display.flip()
📁 Minimal example - Distortion
This is cause, because the bounding rectangle of a rotated image is always greater than the bounding rectangle of the original image (except some rotations by multiples of 90 degrees).
The image gets distort because of the multiply copies. Each rotation generates a small error (inaccuracy). The sum of the errors is growing and the images decays.
That can be fixed by keeping the original image and "blit" an image which was generated by a single rotation operation form the original image.
angle = 0
while not done:
# [...]
rotated_image = pygame.transform.rotate(image, angle)
angle += 1
screen.blit(rotated_image, pos)
pygame.display.flip()
Now the image seems to arbitrary change its position, because the size of the image changes by the rotation and origin is always the top left of the bounding rectangle of the image.
This can be compensated by comparing the axis aligned bounding box of the image before the rotation and after the rotation.
For the following math pygame.math.Vector2
is used. Note in screen coordinates the y-axis points down the screen, but the mathematical y axis points form the bottom to the top. This causes that the y axis has to be "flipped" during calculations
Set up a list with the 4 corner points of the bounding box:
w, h = image.get_size()
box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]
Rotate the vectors to the corner points by pygame.math.Vector2.rotate
:
box_rotate = [p.rotate(angle) for p in box]
Get the minimum and the maximum of the rotated points:
min_box = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])
max_box = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])
Calculate the "compensated" origin of the upper left point of the image by adding the minimum of the rotated box to the position. For the y coordinate max_box[1]
is the minimum, because of the "flipping" along the y axis:
origin = (pos[0] + min_box[0], pos[1] - max_box[1])
rotated_image = pygame.transform.rotate(image, angle)
screen.blit(rotated_image, origin)
📁 Minimal example - Rotate around (0, 0)
It is even possible to define a pivot on the original image. Compute the offset vector from the center of the image to the pivot and rotate the vector. A vector can be represented by pygame.math.Vector2
and can be rotated with pygame.math.Vector2.rotate
. Notice that pygame.math.Vector2.rotate
rotates in the opposite direction than pygame.transform.rotate
. Therefore the angle has to be inverted:
Compute the offset vector from the center of the image to the pivot on the image:
image_rect = image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = pygame.math.Vector2(origin) - image_rect.center
Rotate the offset vector the same angle you want to rotate the image:
rotated_offset = offset_center_to_pivot.rotate(-angle)
Calculate the new center point of the rotated image by subtracting the rotated offset vector from the pivot point in the world:
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
Rotate the image and set the center point of the rectangle enclosing the rotated image. Finally blit the image :
rotated_image = pygame.transform.rotate(image, angle)
rotated_image_rect = rotated_image.get_rect(center = rotated_image_center)
surf.blit(rotated_image, rotated_image_rect)
In the following example program, the function blitRotate(surf, image, pos, originPos, angle)
does all the above steps and "blit" a rotated image to a surface.
-
surf
is the target Surface -
image
is the Surface which has to be rotated andblit
-
pos
is the position of the pivot on the target Surfacesurf
(relative to the top left ofsurf
) -
originPos
is position of the pivot on theimage
Surface (relative to the top left ofimage
) -
angle
is the angle of rotation in degrees
This means, the 2nd argument (pos
) of blitRotate
is the position of the pivot point in the window and the 3rd argument (originPos
) is the position of the pivot point on the rotating Surface:
📁 Minimal example - Rotate around pivot
📁 Minimal example - Rotate around pivot function
repl.it/@Rabbid76/PyGame-RotateAroundPivot
📁 Minimal example - Rotate around pivot and scale
📁 Minimal example - Rotate around pivot and scale function
repl.it/@Rabbid76/PyGame-RotateAroundPivotAndZoom
First the position of the pivot on the Surface
has to be defined:
image = pygame.image.load("boomerang64.png") # 64x64 surface
pivot = (48, 21) # position of the pivot on the image
When an image is rotated, then its size increase. We have to compare the axis aligned bounding box of the image before the rotation and after the rotation.
For the following math pygame.math.Vector2
is used. Note in screen coordinates the y points down the screen, but the mathematical y axis points form the bottom to the top. This causes that the y axis has to be "flipped" during calculations
Set up a list with the 4 corner points of the bounding box and rotate the vectors to the corner points by pygame.math.Vector2.rotate
. Finally find the minimum of the rotated box. Since the y axis in pygame points downwards, this has to be compensated by finding the maximum of the rotated inverted height ("max(rotate(-h))"):
w, h = image.get_size()
box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]
box_rotate = [p.rotate(angle) for p in box]
min_x, min_y = min(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1]
The computation of min_x
and min_y
can be improved by directly computing the x and y component of the rotated vectors by trigonometric functions:
w, h = image.get_size()
sin_a, cos_a = math.sin(math.radians(angle)), math.cos(math.radians(angle))
min_x, min_y = min([0, sin_a*h, cos_a*w, sin_a*h + cos_a*w]), max([0, sin_a*w, -cos_a*h, sin_a*w - cos_a*h])
Calculate the "compensated" origin of the upper left point of the image by adding the minimum of the rotated box to the position in relation to the pivot on the image:
origin = (pos[0] - originPos[0] + min_x - pivot_move[0], pos[1] - originPos[1] - min_y + pivot_move[1])
rotated_image = pygame.transform.rotate(image, angle)
screen.blit(rotated_image, origin)
In the following example program, the function blitRotate(surf, image, pos, originPos, angle)
does all the above steps and blit
a rotated image to the Surface which is associated to the display:
surf
is the target Surfaceimage
is the Surface which has to be rotated andblit
origin
is the position of the pivot on the target Surfacesurf
(relative to the top left ofsurf
)pivot
is position of the pivot on theimage
Surface (relative to the top left ofimage
)angle
is the angle of rotation in degrees
def blitRotate(surf, image, origin, pivot, angle):
image_rect = image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = pygame.math.Vector2(pos) - image_rect.center
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
rotated_image = pygame.transform.rotate(image, angle)
rotated_image_rect = rotated_image.get_rect(center = rotated_image_center)
surf.blit(rotated_image, rotated_image_rect)
📁 Minimal example - Rotate around off center pivot
The same algorithm can be used for a Sprite, too.
In that case the position (self.pos
), pivot (self.pivot
) and angle (self.angle
) are instance attributes of the class.
In the update
method the self.rect
and self.image
attributes are computed. e.g:
📁 Minimal example - Rotate Sprite around off center pivot (boomerang)
repl.it/@Rabbid76/PyGame-RotateSpriteAroundOffCenterPivot
📁 Minimal example - Rotate Sprite around off center pivot (cannon)
repl.it/@Rabbid76/PyGame-RotateSpriteAroundOffCenterPivotCannon
To rotate an image around a pivot point and zoom the image, all you have to do is scale the vector from the center of the image to the pivot point on the image by the zoom factor:
offset_center_to_pivot = pygame.math.Vector2(origin) - image_rect.center
offset_center_to_pivot = (pygame.math.Vector2(origin) - image_rect.center) * scale
The final function that rotates an image around a pivot point, zooms and blit
s the image might look like this:
def blitRotateZoom(surf, original_image, origin, pivot, angle, scale):
image_rect = original_image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = (pygame.math.Vector2(origin) - image_rect.center) * scale
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
rotozoom_image = pygame.transform.rotozoom(original_image, angle, scale)
rect = rotozoom_image.get_rect(center = rotated_image_center)
surf.blit(rotozoom_image, rect)
The scaling factor can also be specified separately for the x and y axis:
def blitRotateZoomXY(surf, original_image, origin, pivot, angle, scale_x, scale_y):
image_rect = original_image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = pygame.math.Vector2(origin) - image_rect.center
offset_center_to_pivot.x *= scale_x
offset_center_to_pivot.y *= scale_y
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
scaled_image = pygame.transform.smoothscale(original_image, (image_rect.width * scale_x, image_rect.height * scale_y))
rotozoom_image = pygame.transform.rotate(scaled_image, angle)
rect = rotozoom_image.get_rect(center = rotated_image_center)
surf.blit(rotozoom_image, rect)
📁 Minimal example - Rotate Sprite around pivot and zoom
repl.it/@Rabbid76/PyGame-RotateZoomPivot
📁 Minimal example - Rotate Sprite around pivot and zoom - cannon
repl.it/@Rabbid76/PyGame-RotateZoomPivot-Example
Another important point when rotating an image is that the image must have an alpha channel. Many people use rectangular, uniformly filled surfaces to test their code, forgetting about the alpha channel. This results in what appears to be a randomly stretched rectangle. How to create a surface with an alpha channel is explained in the answer to the question: pygame doesn't rotate surface. In the example I create the pygame.Surface
with the pygame.SRCALPHA
flag to get an image with an alpha channel:
surface = pygame.Surface((100, 50), pygame.SRCAPLHA)
The second important thing is that you must never rotate a pygame.Surface
object repeatedly, because this leads to an ever increasing pygame.Surface
and distortions, but you must save the original surface and always create a rotated pygame.Surface
from it. See the following example, where I use the pygame.sprite
module