简体   繁体   中英

Stretch an Image in Pygame while preserving the corners

The title says it all really. The effect I'm desiring is going to be used for UI, since UI bubbles will appear, and I want to animate them stretching.

Chat bubbles in iOS messaging apps are a good example of this behavior, see here for example . Here's the main image reproduced:

在此处输入图片说明

Notice the last chat bubbles wonky behavior. This is not normal in messaging apps, and the proper stretching is what I want to achieve with Pygame.

Is there any easy way to reproduce this specific kind of stretching in Pygame? Even if there are some constraints, like, all corners have to be the same size or something. I'd just like to know what is possible.

Thanks!

Based on what I had suggested in the comments, here is an implementation of the SliceSprite class that creates and renders a 9-sliced sprite in pygame. I have also included a sample to show how it might be used. It is definitely rough around the edges (does not check for invalid input like when you resize the sprite with a width less than your defined left and right slice sizes) but should still be a useful start. This code has been updated and polished to handle these edge cases and does not recreate nine subsurfaces on every draw call as suggested by @skrx in the comments.

slicesprite.py

import pygame

class SliceSprite(pygame.sprite.Sprite):
    """
    SliceSprite extends pygame.sprite.Sprite to allow for 9-slicing of its contents.
    Slicing of its image property is set using a slicing tuple (left, right, top, bottom).
    Values for (left, right, top, bottom) are distances from the image edges.
    """
    width_error = ValueError("SliceSprite width cannot be less than (left + right) slicing")
    height_error = ValueError("SliceSprite height cannot be less than (top + bottom) slicing")

    def __init__(self, image, slicing=(0, 0, 0, 0)):
        """
        Creates a SliceSprite object.
        _sliced_image is generated in _generate_slices() only when _regenerate_slices is True.
        This avoids recomputing the sliced image whenever each SliceSprite parameter is changed
        unless absolutely necessary! Additionally, _rect does not have direct @property access
        since updating properties of the rect would not be trigger _regenerate_slices.

        Args:
            image (pygame.Surface): the original surface to be sliced
            slicing (tuple(left, right, top, bottom): the 9-slicing margins relative to image edges
        """
        pygame.sprite.Sprite.__init__(self)
        self._image = image
        self._sliced_image = None
        self._rect = self.image.get_rect()
        self._slicing = slicing
        self._regenerate_slices = True

    @property
    def image(self):
        return self._image

    @image.setter
    def image(self, new_image):
        self._image = new_image
        self._regenerate_slices = True

    @property
    def width(self):
        return self._rect.width

    @width.setter
    def width(self, new_width):
        self._rect.width = new_width
        self._regenerate_slices = True

    @property
    def height(self):
        return self._rect.height

    @height.setter
    def height(self, new_height):
        self._rect.height = new_height
        self._regenerate_slices = True

    @property
    def x(self):
        return self._rect.x

    @x.setter
    def x(self, new_x):
        self._rect.x = new_x
        self._regenerate_slices = True

    @property
    def y(self):
        return self._rect.y

    @y.setter
    def y(self, new_y):
        self._rect.y = new_y
        self._regenerate_slices = True

    @property
    def slicing(self):
        return self._slicing

    @slicing.setter
    def slicing(self, new_slicing=(0, 0, 0, 0)):
        self._slicing = new_slicing
        self._regenerate_slices = True

    def get_rect(self):
        return self._rect

    def set_rect(self, new_rect):
        self._rect = new_rect
        self._regenerate_slices = True

    def _generate_slices(self):
        """
        Internal method required to generate _sliced_image property.
        This first creates nine subsurfaces of the original image (corners, edges, and center).
        Next, each subsurface is appropriately scaled using pygame.transform.smoothscale.
        Finally, each subsurface is translated in "relative coordinates."
        Raises appropriate errors if rect cannot fit the center of the original image.
        """
        num_slices = 9
        x, y, w, h = self._image.get_rect()
        l, r, t, b = self._slicing
        mw = w - l - r
        mh = h - t - b
        wr = w - r
        hb = h - b

        rect_data = [
            (0, 0, l, t), (l, 0, mw, t), (wr, 0, r, t),
            (0, t, l, mh), (l, t, mw, mh), (wr, t, r, mh),
            (0, hb, l, b), (l, hb, mw, b), (wr, hb, r, b),
        ]

        x, y, w, h = self._rect
        mw = w - l - r
        mh = h - t - b
        if mw < 0: raise SliceSprite.width_error
        if mh < 0: raise SliceSprite.height_error

        scales = [
            (l, t), (mw, t), (r, t),
            (l, mh), (mw, mh), (r, mh),
            (l, b), (mw, b), (r, b),
        ]

        translations = [
            (0, 0), (l, 0), (l + mw, 0),
            (0, t), (l, t), (l + mw, t),
            (0, t + mh), (l, t + mh), (l + mw, t + mh),
        ]

        self._sliced_image = pygame.Surface((w, h))
        for i in range(num_slices):
            rect = pygame.rect.Rect(rect_data[i])
            surf_slice = self.image.subsurface(rect)
            stretched_slice = pygame.transform.smoothscale(surf_slice, scales[i])
            self._sliced_image.blit(stretched_slice, translations[i])

    def draw(self, surface):
        """
        Draws the SliceSprite onto the desired surface.
        Calls _generate_slices only at draw time only if necessary.
        Note that the final translation occurs here in "absolute coordinates."

        Args:
            surface (pygame.Surface): the parent surface for blitting SliceSprite
        """
        x, y, w, h, = self._rect
        if self._regenerate_slices:
            self._generate_slices()
            self._regenerate_slices = False
        surface.blit(self._sliced_image, (x, y))

Example usage (main.py):

import pygame
from slicesprite import SliceSprite

if __name__ == "__main__":
    pygame.init()
    screen = pygame.display.set_mode((800, 600))
    clock = pygame.time.Clock()
    done = False

    outer_points = [(0, 20), (20, 0), (80, 0), (100, 20), (100, 80), (80, 100), (20, 100), (0, 80)]
    inner_points = [(10, 25), (25, 10), (75, 10), (90, 25), (90, 75), (75, 90), (25, 90), (10, 75)]
    image = pygame.Surface((100, 100), pygame.SRCALPHA)
    pygame.draw.polygon(image, (20, 100, 150), outer_points)
    pygame.draw.polygon(image, (0, 60, 120), inner_points)

    button = SliceSprite(image, slicing=(25, 25, 25, 25))
    button.set_rect((50, 100, 500, 200))
    #Alternate version if you hate using rects for some reason
    #button.x = 50
    #button.y = 100
    #button.width = 500
    #button.height = 200

    while not done:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                done = True
        screen.fill((0, 0, 0))
        button.draw(screen)
        pygame.display.flip()
        clock.tick()

Here's a solution in which I create an enlarged version of the surface by splitting it into three parts and blitting the middle line repeatedly. Vertical enlargement would work similarly.

import pygame as pg


def enlarge_horizontal(image, width=None):
    """A horizontally enlarged version of the image.

    Blit the middle line repeatedly to enlarge the image.

    Args:
        image (pygame.Surface): The original image/surface.
        width (int): Desired width of the scaled surface.
    """
    w, h = image.get_size()
    # Just return the original image, if the desired width is too small.
    if width is None or width < w:
        return image
    mid_point = w//2
    # Split the image into 3 parts (left, mid, right).
    # `mid` is just the middle vertical line.
    left = image.subsurface((0, 0, w//2, h))
    mid = image.subsurface((mid_point, 0, 1, h))
    right = image.subsurface((mid_point, 0, w//2, h))
    surf = pg.Surface((width, h), pg.SRCALPHA)

    # Join the parts (blit them onto the new surface).
    surf.blit(left, (0, 0))
    for i in range(width-w+1):
        surf.blit(mid, (mid_point+i, 0))
    surf.blit(right, (width-w//2, 0))
    return surf


def main():
    screen = pg.display.set_mode((800, 800))
    clock = pg.time.Clock()
    image = pg.Surface((100, 100), pg.SRCALPHA)
    pg.draw.circle(image, (20, 100, 150), (50, 50), 50)
    pg.draw.circle(image, (0, 60, 120), (50, 50), 45)

    surfs = [enlarge_horizontal(image, width=i) for i in range(0, 701, 140)]

    while True:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                return

        screen.fill((30, 30, 40))
        for i, surf in enumerate(surfs):
            screen.blit(surf, (20, i*109 + 5))
        pg.display.flip()
        clock.tick(60)


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM