How do I create an extendable bar sprite in PyGame?

Question:

I want to use the following asset in my game:
bar asset

It has specific pixelated borders and corners. So I want it to scale "extending" the inner part of the asset by repeating it until it matches the desired size. So I can render something like this:
extended bar sprite

This technique is quite common in UI building world, when you create this simple asset to fit buttons of any size.

Is there any way to achieve this with built in PyGame tools?

Asked By: kokoko

||

Answers:

Pygame can only scale a complete image. There is no way to scale a part of an image or to stretch an image at a certain point. You have to implement it yourself.
Divide the image into 3 parts, the beginning, the middle and the end. Scale the middle part and stick the parts together again. e.g.:

import pygame

pygame.init()
window = pygame.display.set_mode((200, 100))
clock = pygame.time.Clock()

bar = pygame.image.load("ORv9A.png")

def scale_bar(image, width):
    size = image.get_size()
    margin = 4
    middel_parat = image.subsurface(pygame.Rect(margin, 0, size[0]-margin*2, size[1]))
    scaled_image = pygame.Surface((width, size[1]))
    scaled_image.blit(image, (0, 0), (0, 0, margin, size[1]))
    scaled_image.blit(pygame.transform.smoothscale(middel_parat, (width-margin*2, size[1])), (margin, 0))
    scaled_image.blit(image, (width-margin, 0), (size[0]-margin, 0, margin, size[1]) )
    return scaled_image

run = True
while run:
    clock.tick(100)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False 

    scaled_image = scale_bar(bar, 100)

    window.fill(0)
    window.blit(scaled_image, scaled_image.get_rect(center = window.get_rect().center))
    pygame.display.flip()

pygame.quit()
exit()
Answered By: Rabbid76

I came up with my custom solution which replicates Unity 9-slice sprite technique.

The following method uses 9-slice parameters as well as the desired size and draw method.

Rendering modes

To make this method as close to Unity as possible I created two modes:

  • SLICED – Draw corners of the sprite and scale the content and edges
  • TILED – Draw corners of the sprite and tile edges and content

The following example uses this test asset to demonstrate rendering differences: test asset with parts indications

import pygame
from pygame.locals import *
from enum import Enum

pygame.init()

clock = pygame.time.Clock()
fps = 60

# Set up the drawing window
screen = pygame.display.set_mode([300, 300])
pygame.display.set_caption('Sprite Slicing Example')

# load test sprite
sprite = pygame.image.load('res/test.png')

# define draw mode enums
DrawMode = Enum('DrawMode', ['SLICED', 'TILED'])

# 9-slice a sprite to the desired size
def slice_sprite(sprite, left, right, top, bottom, width, height, draw_mode=DrawMode.TILED):
    # get the size of the sprite
    sprite_width = sprite.get_width()
    sprite_height = sprite.get_height()

    # create a new surface to draw the sliced sprite on
    sliced_sprite = pygame.Surface((width, height))

    # draw the top left side of the sprite
    sliced_sprite.blit(sprite, (0, 0), (0, 0, left, top))

    # draw the top right side of the sprite
    sliced_sprite.blit(sprite, (width - right, 0),
                       (sprite_width - right, 0, right, top))

    # draw the bottom left side of the sprite
    sliced_sprite.blit(sprite, (0, height - bottom),
                       (0, sprite_height - bottom, left, bottom))

    # draw the bottom right side of the sprite
    sliced_sprite.blit(sprite, (width - right, height - bottom),
                       (sprite_width - right, sprite_height - bottom, right, bottom))

    match draw_mode:
        case DrawMode.SLICED:
            # scale top and bottom sides of the sprite
            sliced_sprite.blit(pygame.transform.scale(sprite.subsurface(
                left, 0, sprite_width - left - right, top), (width - left - right, top)), (left, 0))
            sliced_sprite.blit(pygame.transform.scale(sprite.subsurface(left, sprite_height - bottom,
                               sprite_width - left - right, bottom), (width - left - right, bottom)), (left, height - bottom))

            # scale the center of the sprite
            sliced_sprite.blit(pygame.transform.scale(sprite.subsurface(left, top, sprite_width - left - right,
                               sprite_height - top - bottom), (width - left - right, height - top - bottom)), (left, top))

            # scale left and right sides of the sprite
            sliced_sprite.blit(pygame.transform.scale(sprite.subsurface(
                0, top, left, sprite_height - top - bottom), (left, height - top - bottom)), (0, top))
            sliced_sprite.blit(pygame.transform.scale(sprite.subsurface(sprite_width - right, top, right,
                               sprite_height - top - bottom), (right, height - top - bottom)), (width - right, top))

        case DrawMode.TILED:
            # tile the center of the sprite
            for x in range(left, width - right, sprite_width - left - right):
                for y in range(top, height - bottom, sprite_height - top - bottom):
                    sliced_sprite.blit(sprite.subsurface(
                        left, top, sprite_width - left - right, sprite_height - top - bottom), (x, y))

            # tile top and bottom sides of the sprite
            for x in range(left, width - right, sprite_width - left - right):
                sliced_sprite.blit(sprite.subsurface(
                    left, 0, sprite_width - left - right, top), (x, 0))
                sliced_sprite.blit(sprite.subsurface(
                    left, sprite_height - bottom, sprite_width - left - right, bottom), (x, height - bottom))

            # tile left and right sides of the sprite
            for y in range(top, height - bottom, sprite_height - top - bottom):
                sliced_sprite.blit(sprite.subsurface(
                    0, top, left, sprite_height - top - bottom), (0, y))
                sliced_sprite.blit(sprite.subsurface(
                    sprite_width - right, top, right, sprite_height - top - bottom), (width - right, y))

            # draw the corners of the sprite
            sliced_sprite.blit(sprite.subsurface(0, 0, left, top), (0, 0))
            sliced_sprite.blit(sprite.subsurface(
                sprite_width - right, 0, right, top), (width - right, 0))
            sliced_sprite.blit(sprite.subsurface(
                0, sprite_height - bottom, left, bottom), (0, height - bottom))
            sliced_sprite.blit(sprite.subsurface(
                sprite_width - right, sprite_height - bottom, right, bottom), (width - right, height - bottom))

    return sliced_sprite


run = True
while run:

    clock.tick(fps)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    # Draw the sprite
    screen.blit(slice_sprite(sprite, 8, 8, 8, 8, 100, 100), (100, 100))

    # Flip the display
    pygame.display.flip()

pygame.quit()

The resulting sprite with SLICED rendering:
enter image description here

The same sprite with TILED rendering:
enter image description here

Other examples

Asset: enter image description here
slice_sprite(bubble, 4, 9, 4, 6, 100, 50, draw_mode=DrawMode.TILED)
enter image description here

Asset: enter image description here
slice_sprite(bar, 8, 8, 8, 8, 30, 200, draw_mode=DrawMode.SLICED)
enter image description here
slice_sprite(bar, 8, 8, 8, 8, 200, 50, draw_mode=DrawMode.SLICED)
enter image description here

Answered By: kokoko