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

I have a set of keyframes in a list that look like this:

   [{
        "duration" : 20,
        "position" : [0,0],
        "scale" : [1, 1],
        "angle" : 0,
        "rgba" : [255,255,255,255]
    },
    {
        "duration" : 5,
        "position" : [0,0],
        "scale" : [1, 1.5],
        "angle" : 50,
        "rgba" : [255,255,255,255]
    }]

The idea is being able to do the corresponding transformations every frame. Notice that scale is separated between width and height.
The problem comes form trying to scale width and height independently, while still rotating around a pivot.

I tried modifying some code from: (How to rotate an image around its center while its scale is getting larger(in Pygame))

def blitRotate(surf, image, pos, originPos, angle, zoom):

    # 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
    move   = (-originPos[0] + min_box[0] - pivot_move[0], -originPos[1] - max_box[1] + pivot_move[1])
origin = (pos[0] + zoom * move[0], pos[1] + zoom * move[1])

# get a rotated image
rotozoom_image = pygame.transform.rotozoom(image, angle, zoom)

# rotate and blit the image
surf.blit(rotozoom_image, origin)

# draw rectangle around the image
pygame.draw.rect (surf, (255, 0, 0), (*origin, *rotozoom_image.get_size()),2)

but i'm struggling trying to think of the math necessary to make it work, i've tried separating zoom into a dupe, and then instead of doing rotozoom , scaling first with transform.scale and then transform.rotate afterwards but that didn't work either.

To better illustrate what i mean, it would be something like this:
rotating around pivot while changing width and height

It changes it's width and height but the pivot stays the same


Solution 1:

I would suggest adopting a slightly different approach presented here: How to set the pivot point (center of rotation) for pygame.transform.rotate()?

All you have to do to adjust this algorithm is scale the vector from the image center 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 blitRotate(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 blitRotate(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)

See also Rotate surface


Minimal example: repl.it/@Rabbid76/PyGame-RotateZoomPivot

import pygame

pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()

def blitRotate(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)
    pygame.draw.rect (surf, (255, 0, 0), rect, 2)

try:
    image = pygame.image.load('AirPlane.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()

angle, zoom = 0, 1
done = False
while not done:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    pos = (screen.get_width()/2, screen.get_height()/2)
    
    screen.fill(0)
    blitRotate(screen, image, pos, (w/4, h/2), angle, zoom)
    angle += 1
    zoom += 0.01
    if zoom > 5:
        zoom = 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()

Example 2: repl.it/@Rabbid76/PyGame-RotateZoomPivot

import pygame

pygame.init()
screen = pygame.display.set_mode((400, 300))
clock = pygame.time.Clock()

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)

cannon = pygame.image.load('icon/cannon.png')
cannon_mount = pygame.image.load('icon/cannon_mount.png')

angle, zoom_x, zoom_y = -90, 1, 1
stage = 0
done = False
while not done:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    pos = (screen.get_width()/3, screen.get_height()*3/4)
    
    screen.fill((192, 192, 192))
    blitRotateZoomXY(screen, cannon, pos, (33.5, 120), angle, zoom_x, zoom_y)
    screen.blit(cannon_mount, (pos[0]-43, pos[1]-16))
    pygame.display.flip()

    if stage == 0:
        angle += 1
        if angle >= -30:
            stage += 1
    elif stage == 1:
        zoom_y -= 0.05
        if zoom_y <= 0.7:
           stage += 1
    elif stage == 2: 
        zoom_y += 0.05
        if zoom_y >= 1:
           stage += 1
    elif stage == 3:
        angle -= 1
        if angle <= -90:
            stage = 0
    
pygame.quit()
exit()