在 Pygame 中拉伸图像,同时保留边角
Stretch an Image in Pygame while preserving the corners
标题说明了一切。我想要的效果将用于 UI,因为会出现 UI 个气泡,我想为它们设置拉伸动画。
iOS 消息应用程序中的聊天气泡就是这种行为的一个很好的例子,请参阅
。这是转载的主图:
请注意上次聊天气泡的异常行为。这在消息传递应用程序中是不正常的,适当的拉伸是我想要通过 Pygame.
实现的
有什么简单的方法可以在 Pygame 中重现这种特定类型的拉伸?即使有一些限制,比如所有的角都必须是相同的大小之类的。我只是想知道什么是可能的。
谢谢!
根据我在评论中的建议,这里是 SliceSprite
class 的一个实现,它在 pygame 中创建和渲染一个 9 切片精灵。我还提供了一个示例来展示如何使用它。 它的边缘确实很粗糙(不会检查无效输入,就像当您调整宽度小于您定义的左右切片大小的精灵大小时)但仍然应该是一个有用的开始。 此代码已更新和完善以处理这些边缘情况,并且不会像 @skrx 在评论中建议的那样在每次绘制调用时重新创建九个子表面。
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))
用法示例 (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()
这是一个解决方案,我通过将表面分成三部分并重复 blits 中间线来创建放大版本的表面。垂直放大的效果类似。
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()
标题说明了一切。我想要的效果将用于 UI,因为会出现 UI 个气泡,我想为它们设置拉伸动画。
iOS 消息应用程序中的聊天气泡就是这种行为的一个很好的例子,请参阅
请注意上次聊天气泡的异常行为。这在消息传递应用程序中是不正常的,适当的拉伸是我想要通过 Pygame.
实现的有什么简单的方法可以在 Pygame 中重现这种特定类型的拉伸?即使有一些限制,比如所有的角都必须是相同的大小之类的。我只是想知道什么是可能的。
谢谢!
根据我在评论中的建议,这里是 SliceSprite
class 的一个实现,它在 pygame 中创建和渲染一个 9 切片精灵。我还提供了一个示例来展示如何使用它。 它的边缘确实很粗糙(不会检查无效输入,就像当您调整宽度小于您定义的左右切片大小的精灵大小时)但仍然应该是一个有用的开始。 此代码已更新和完善以处理这些边缘情况,并且不会像 @skrx 在评论中建议的那样在每次绘制调用时重新创建九个子表面。
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))
用法示例 (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()
这是一个解决方案,我通过将表面分成三部分并重复 blits 中间线来创建放大版本的表面。垂直放大的效果类似。
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()