PyGameExamplesAndAnswers

StackOverflow            reply.it reply.it

“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


Rotate surface

Related Stack Overflow questions:

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

rotate 1

📁 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()

rotate 2

📁 Minimal example - Rotate

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)

rotate 3

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

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:

rotate 4

📁 Minimal example - Rotate around pivot

📁 Minimal example - Rotate around pivot function

repl.it/@Rabbid76/PyGame-RotateAroundPivot

roatate and scale

📁 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:

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)

rotate

📁 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 blits 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)

Rotating and scaling an image around a pivot, while scaling width and height separately in Pygame

📁 Minimal example - Rotate Sprite around pivot and zoom
repl.it/@Rabbid76/PyGame-RotateZoomPivot

Rotating and scaling an image around a pivot, while scaling width and height separately in Pygame

📁 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

📁 Minimal example - Rotate Sprite