How do I rotate an image around its center using Pygame?

Posted on

Solving problem is about exposing yourself to as many situations as possible like How do I rotate an image around its center using Pygame? and practice these strategies over and over. With time, it becomes second nature and a natural way you approach any problems in general. Big or small, always start with a plan, use other strategies mentioned here till you are confident and ready to code the solution.
In this post, my aim is to share an overview the topic about How do I rotate an image around its center using Pygame?, which can be followed any time. Take easy to follow this discuss.

How do I rotate an image around its center using Pygame?

I had been trying to rotate an image around its center in using pygame.transform.rotate() but it’s not working. Specifically the part that hangs is rot_image = rot_image.subsurface(rot_rect).copy(). I get the exception:

ValueError: subsurface rectangle outside surface area

Here is the code used to rotate an image:

def rot_center(image, angle):
    """rotate an image while keeping its center and size"""
    orig_rect = image.get_rect()
    rot_image = pygame.transform.rotate(image, angle)
    rot_rect = orig_rect.copy()
    rot_rect.center = rot_image.get_rect().center
    rot_image = rot_image.subsurface(rot_rect).copy()
    return rot_image
Asked By: Miha

||

Answer #1:

Short answer:

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. Return a tuple of the rotated image and the rectangle:

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

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

It is even possible to define a pivot on the original image. The “translation” of the pivot in relation to the upper left of the image has to be calculated and the “blit” position of the image has to be displaced by the translation.

Define a pivot e.g. in the center of the image:

pivot = pygame.math.Vector2(w/2, -h/2)

Calculate the translation of the rotated pivot:

pivot_rotate = pivot.rotate(angle)
pivot_move   = pivot_rotate - pivot

Finally calculate the origin of the rotated image:

origin = (pos[0] + min_box[0] - pivot_move[0], pos[1] - max_box[1] + 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 a surface.

  • surf is the target Surface

  • image is the Surface which has to be rotated and blit

  • pos is the position of the pivot on the target Surface surf (relative to the top left of surf)

  • originPos is position of the pivot on the image Surface (relative to the top left of image)

  • 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: repl.it/@Rabbid76/PyGame-RotateAroundPivot

import pygame
pygame.init()
screen = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
def blitRotate(surf, image, pos, originPos, angle):
    # calcaulate the axis aligned bounding box of the rotated image
    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_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 translation of the pivot 
    pivot        = pygame.math.Vector2(originPos[0], -originPos[1])
    pivot_rotate = pivot.rotate(angle)
    pivot_move   = pivot_rotate - pivot
    # calculate the upper left origin of the rotated image
    origin = (pos[0] - originPos[0] + min_box[0] - pivot_move[0], pos[1] - originPos[1] - max_box[1] + pivot_move[1])
    # get a rotated image
    rotated_image = pygame.transform.rotate(image, angle)
    # rotate and blit the image
    surf.blit(rotated_image, origin)
    # draw rectangle around the image
    pygame.draw.rect(surf, (255, 0, 0), (*origin, *rotated_image.get_size()),2)
def blitRotate2(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)
    pygame.draw.rect(surf, (255, 0, 0), new_rect, 2)
try:
    image = pygame.image.load('AirPlaneFront1-128.png')
except:
    text = pygame.font.SysFont('Times New Roman', 50).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))
w, h = image.get_size()
start = False
angle = 0
done = False
while not done:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        elif event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN:
            start = True
    pos = (screen.get_width()/2, screen.get_height()/2)
    screen.fill(0)
    blitRotate(screen, image, pos, (w/2, h/2), angle)
    #blitRotate2(screen, image, pos, angle)
    if start:
        angle += 1
    pygame.draw.line(screen, (0, 255, 0), (pos[0]-20, pos[1]), (pos[0]+20, pos[1]), 3)
    pygame.draw.line(screen, (0, 255, 0), (pos[0], pos[1]-20), (pos[0], pos[1]+20), 3)
    pygame.draw.circle(screen, (0, 255, 0), pos, 7, 0)
    pygame.display.flip()
pygame.quit()
exit()

See also Rotate surface and the answers to the questions:

Answered By: Rabbid76

Answer #2:

You are deleting the rect that rotate creates. You need to preserve rect, since it changes size when rotated.

If you want to preserve the objects location, do:

def rot_center(image, angle):
    """rotate a Surface, maintaining position."""
    loc = image.get_rect().center  #rot_image is not defined 
    rot_sprite = pygame.transform.rotate(image, angle)
    rot_sprite.get_rect().center = loc
    return rot_sprite
    # or return tuple: (Surface, Rect)
    # return rot_sprite, rot_sprite.get_rect()
Answered By: ninMonkey

Answer #3:

There are some problems with the top answer: The position of the previous rect needs to be available in the function, so that we can assign it to the new rect, e.g.:

rect = new_image.get_rect(center=rect.center)

In the other answer the location is obtained by creating a new rect from the original image, but that means it will be positioned at the default (0, 0) coordinates.

The example below should work correctly. The new rect needs the center position of the old rect, so we pass it as well to the function. Then rotate the image, call get_rect to get a new rect with the correct size and pass the center attribute of the old rect as the center argument. Finally, return both the rotated image and the new rect as a tuple and unpack it in the main loop.

import pygame as pg
def rotate(image, rect, angle):
    """Rotate the image while keeping its center."""
    # Rotate the original image without modifying it.
    new_image = pg.transform.rotate(image, angle)
    # Get a new rect with the center of the old rect.
    rect = new_image.get_rect(center=rect.center)
    return new_image, rect
def main():
    clock = pg.time.Clock()
    screen = pg.display.set_mode((640, 480))
    gray = pg.Color('gray15')
    blue = pg.Color('dodgerblue2')
    image = pg.Surface((320, 200), pg.SRCALPHA)
    pg.draw.polygon(image, blue, ((0, 0), (320, 100), (0, 200)))
    # Keep a reference to the original to preserve the image quality.
    orig_image = image
    rect = image.get_rect(center=(320, 240))
    angle = 0
    done = False
    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
        angle += 2
        image, rect = rotate(orig_image, rect, angle)
        screen.fill(gray)
        screen.blit(image, rect)
        pg.display.flip()
        clock.tick(30)
if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()

Here’s another example with a rotating pygame sprite.

import pygame as pg
class Entity(pg.sprite.Sprite):
    def __init__(self, pos):
        super().__init__()
        self.image = pg.Surface((122, 70), pg.SRCALPHA)
        pg.draw.polygon(self.image, pg.Color('dodgerblue1'),
                        ((1, 0), (120, 35), (1, 70)))
        # A reference to the original image to preserve the quality.
        self.orig_image = self.image
        self.rect = self.image.get_rect(center=pos)
        self.angle = 0
    def update(self):
        self.angle += 2
        self.rotate()
    def rotate(self):
        """Rotate the image of the sprite around its center."""
        # `rotozoom` usually looks nicer than `rotate`. Pygame's rotation
        # functions return new images and don't modify the originals.
        self.image = pg.transform.rotozoom(self.orig_image, self.angle, 1)
        # Create a new rect with the center of the old rect.
        self.rect = self.image.get_rect(center=self.rect.center)
def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    all_sprites = pg.sprite.Group(Entity((320, 240)))
    while True:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                return
        all_sprites.update()
        screen.fill((30, 30, 30))
        all_sprites.draw(screen)
        pg.display.flip()
        clock.tick(30)
if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()
Answered By: skrx

Answer #4:

Found the problem: Example works good, but needs equal dimensions for width and height. Fixed pictures and it works.

Answered By: Miha

Answer #5:

Everything you need for drawing an image in pygame

game_display = pygame.display.set_mode((800, 600))
x = 0
y = 0
angle = 0
img = pygame.image.load("resources/image.png")
img = pygame.transform.scale(img, (50, 50)) # image size
def draw_img(self, image, x, y, angle):
    rotated_image = pygame.transform.rotate(image, angle)
    game_display.blit(rotated_image, rotated_image.get_rect(center=image.get_rect(topleft=(x, y)).center).topleft)
# run this method with your loop
def tick():
    draw_img(img, x, y, angle)
Answered By: stexe

Answer #6:

I had to modify skrx solution as below, this way works for me.

angle=0
roll = true
while roll:
    # clean surface with your background color
    gameDisplay.fill(color)
    self.image = yourImage
    rotate_image = pygame.transform.rotate(self.image, angle)
    rect = rotate_image.get_rect()
    pos = (((your_surface_width - rect.width)/2),((your_surface_height - rect.height)/2))
    gameDisplay.blit(rotate_image,pos)
    pygame.display.flip()
    angle+=2
    if angle == 360:
        roll=False
Answered By: Salvatore Fiengo
The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .

Leave a Reply

Your email address will not be published. Required fields are marked *