Skip to content

Commit 70f6bde

Browse files
authored
Add 9-patch to examples (#2985)
* Add 9-patch to examples * Pre-commit didn't work the first time * Update example after review * Improve example
1 parent 295a033 commit 70f6bde

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed

examples/data/frame.png

253 Bytes
Loading

examples/ninepatch.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python
2+
"""pygame.examples.ninepatch
3+
Demonstrate the purpose of the 9-patch scale method and a way to implement it.
4+
5+
9-patch scaling allows us to scale a surface while keeping 4 corners intact.
6+
This is very useful for GUI applications.
7+
8+
How it works:
9+
The 4 corners are extracted from the original surface and pasted on the new surface.
10+
The 4 sides in between the corners are scaled horizontally or vertically to fit
11+
between the new surface corners.
12+
The central part of the image is scaled and pasted.
13+
14+
In this example a green fill is used which can be done faster and easier with pygame.draw.rect
15+
but usually you'd have a custom surface that needs intact corners.
16+
"""
17+
18+
import os
19+
import pygame
20+
import typing
21+
22+
SCREEN_SIZE = pygame.Vector2(600, 500)
23+
SCALE_SIZE = pygame.Vector2(500, 150)
24+
INNER_SCALE_SIZE = pygame.Vector2(400, 115)
25+
CORNER_SIZE = 20
26+
INNER_CORNER_SIZE = 7
27+
28+
29+
def ninepatch_scale(
30+
surface: pygame.Surface,
31+
size: typing.Sequence[int],
32+
corner_size: int,
33+
alpha: bool = True,
34+
smooth: bool = False,
35+
dest_surface: pygame.Surface | None = None,
36+
) -> pygame.Surface:
37+
# the code of this function also emulates how a transform-like ninepatch function would likely behave
38+
if dest_surface is not None:
39+
ret_surface = dest_surface
40+
if ret_surface.size != size:
41+
raise ValueError("Destination surface doesn't match the provided size")
42+
else:
43+
ret_surface = pygame.Surface(
44+
size, pygame.SRCALPHA * alpha
45+
) # when alpha is False the flags become 0
46+
47+
# aliases
48+
ret_w, ret_h = size # non-sequence argument catched by python
49+
src_w, src_h = surface.size
50+
c = corner_size
51+
scale_func = pygame.transform.smoothscale if smooth else pygame.transform.scale
52+
subsurface = surface.subsurface # alias for performance/clarity
53+
54+
if corner_size < 0 or corner_size > min(ret_w, ret_h):
55+
raise ValueError(
56+
"Corner size must be nonnegative and not greater than the smaller between width and height"
57+
)
58+
if corner_size == 0: # default to normal scaling
59+
return scale_func(surface, size)
60+
61+
src_x_side, src_y_side = surface.size - pygame.Vector2(c) * 2
62+
ret_x_side, ret_y_side = size - pygame.Vector2(c) * 2
63+
64+
ret_surface.blit( # topleft corner
65+
subsurface(0, 0, c, c), (0, 0)
66+
)
67+
ret_surface.blit( # topright corner
68+
subsurface(src_w - c, 0, c, c), (ret_w - c, 0)
69+
)
70+
ret_surface.blit( # bottomleft corner
71+
subsurface(0, src_h - c, c, c), (0, ret_h - c)
72+
)
73+
ret_surface.blit( # bottomright corner
74+
subsurface(src_w - c, src_h - c, c, c), (ret_w - c, ret_h - c)
75+
)
76+
77+
if src_x_side > 0:
78+
ret_surface.blit( # top side
79+
scale_func(subsurface(c, 0, src_x_side, c), (ret_x_side, c)), (c, 0)
80+
)
81+
ret_surface.blit( # bottom side
82+
scale_func(subsurface(c, src_h - c, src_x_side, c), (ret_x_side, c)),
83+
(c, ret_h - c),
84+
)
85+
86+
if src_y_side > 0:
87+
ret_surface.blit( # left side
88+
scale_func(subsurface(0, c, c, src_y_side), (c, ret_y_side)), (0, c)
89+
)
90+
ret_surface.blit( # right side
91+
scale_func(subsurface(src_w - c, c, c, src_y_side), (c, ret_y_side)),
92+
(ret_w - c, c),
93+
)
94+
95+
if src_x_side > 0 and src_y_side > 0:
96+
ret_surface.blit( # central area
97+
scale_func(
98+
subsurface(c, c, src_x_side, src_y_side), (ret_x_side, ret_y_side)
99+
),
100+
(c, c),
101+
)
102+
103+
return ret_surface
104+
105+
106+
def main():
107+
pygame.init()
108+
screen = pygame.display.set_mode(SCREEN_SIZE)
109+
pygame.display.set_caption("9-Patch Scale and Normal Scale Example")
110+
clock = pygame.Clock()
111+
font = pygame.Font(None, 30)
112+
113+
main_dir = os.path.split(os.path.abspath(__file__))[0]
114+
example_image = pygame.image.load(os.path.join(main_dir, "data", "frame.png"))
115+
116+
original_surface = pygame.Surface((100, 100), pygame.SRCALPHA)
117+
pygame.draw.rect(
118+
original_surface, "green", original_surface.get_rect(), 0, CORNER_SIZE
119+
)
120+
121+
normal_scale = pygame.transform.scale(original_surface, SCALE_SIZE)
122+
normal_center = SCREEN_SIZE.elementwise() / pygame.Vector2(2, 4)
123+
normal_rect = normal_scale.get_rect(center=normal_center)
124+
example_normal_scale = pygame.transform.scale(example_image, INNER_SCALE_SIZE)
125+
example_normal_rect = example_normal_scale.get_rect(center=normal_center)
126+
127+
ninepatch = ninepatch_scale(
128+
original_surface, SCALE_SIZE, CORNER_SIZE, True, False, None
129+
)
130+
ninepatch_center = SCREEN_SIZE.elementwise() / pygame.Vector2(2, 4 / 3)
131+
ninepatch_rect = ninepatch.get_rect(center=ninepatch_center)
132+
example_ninepatch = ninepatch_scale(
133+
example_image, INNER_SCALE_SIZE, INNER_CORNER_SIZE, True, False, None
134+
)
135+
example_ninepatch_rect = example_ninepatch.get_rect(center=ninepatch_center)
136+
137+
text_normal = font.render("Normal Scale", True, "black")
138+
text_normal_rect = text_normal.get_rect(center=normal_center)
139+
text_9patch = font.render("9-Patch Scale", True, "black")
140+
text_9patch_rect = text_9patch.get_rect(center=ninepatch_center)
141+
142+
running = True
143+
while running:
144+
for event in pygame.event.get():
145+
if event.type == pygame.QUIT:
146+
running = False
147+
148+
screen.fill(0)
149+
150+
screen.blit(normal_scale, normal_rect)
151+
screen.blit(example_normal_scale, example_normal_rect)
152+
screen.blit(ninepatch, ninepatch_rect)
153+
screen.blit(example_ninepatch, example_ninepatch_rect)
154+
screen.blit(text_normal, text_normal_rect)
155+
screen.blit(text_9patch, text_9patch_rect)
156+
157+
pygame.display.flip()
158+
clock.tick(60)
159+
160+
pygame.quit()
161+
162+
163+
if __name__ == "__main__":
164+
main()

0 commit comments

Comments
 (0)