NinePatchRect¶
9-slice panel scaling.
▶ Run in browserTags: 2d
A nine-patch draws one small texture at any size without blurring its border.
NinePatchRect cuts the source into nine regions with four margins
(patch_margin_left, patch_margin_top, patch_margin_right and
patch_margin_bottom): the four corners are drawn at their authored pixel
size, the top and bottom edges grow only horizontally, the left and right edges
only vertically, and the centre fills what is left. That is how dialogue boxes,
buttons and speech bubbles are drawn from one piece of art.
The source here is a 64x64 RGBA numpy array built in code and handed straight
to texture, which also takes a file path, PNG bytes or a Texture. Its
corners are red, its top and bottom edges green, its sides blue, and each edge
and the centre carry a white or yellow mark so that repetition is visible.
The top row draws that panel at three sizes with the default "stretch"
mode: the corners never change size. The middle row sets both
axis_stretch_horizontal and axis_stretch_vertical to each mode:
"stretch" smears one copy of each edge across the span, "tile" repeats
it at its authored size and clips the last copy, and "tile_fit" repeats a
whole number of copies, resizing them to divide the span exactly. The bottom
row sets one axis at a time: the horizontal mode governs the top edge, bottom
edge and centre, the vertical mode the two sides and the centre.
draw_center=False on the last panel leaves a frame to draw over content.
Every panel is a Node2D, so the layout is recomputed from viewport_size
each frame and follows the window.
Source¶
1# /// simvx
2# web = { width = 900, height = 600 }
3# ///
4"""NinePatchRect: 9-slice panel scaling.
5
6A nine-patch draws one small texture at any size without blurring its border.
7``NinePatchRect`` cuts the source into nine regions with four margins
8(``patch_margin_left``, ``patch_margin_top``, ``patch_margin_right`` and
9``patch_margin_bottom``): the four corners are drawn at their authored pixel
10size, the top and bottom edges grow only horizontally, the left and right edges
11only vertically, and the centre fills what is left. That is how dialogue boxes,
12buttons and speech bubbles are drawn from one piece of art.
13
14The source here is a 64x64 RGBA ``numpy`` array built in code and handed straight
15to ``texture``, which also takes a file path, PNG bytes or a ``Texture``. Its
16corners are red, its top and bottom edges green, its sides blue, and each edge
17and the centre carry a white or yellow mark so that repetition is visible.
18
19The top row draws that panel at three sizes with the default ``"stretch"``
20mode: the corners never change size. The middle row sets both
21``axis_stretch_horizontal`` and ``axis_stretch_vertical`` to each mode:
22``"stretch"`` smears one copy of each edge across the span, ``"tile"`` repeats
23it at its authored size and clips the last copy, and ``"tile_fit"`` repeats a
24whole number of copies, resizing them to divide the span exactly. The bottom
25row sets one axis at a time: the horizontal mode governs the top edge, bottom
26edge and centre, the vertical mode the two sides and the centre.
27``draw_center=False`` on the last panel leaves a frame to draw over content.
28
29Every panel is a ``Node2D``, so the layout is recomputed from ``viewport_size``
30each frame and follows the window.
31"""
32
33import numpy as np
34
35from simvx.core import NinePatchRect, Node2D, Text2D
36from simvx.graphics import App
37
38MARGIN = 16
39PAD = 20
40
41# (caption, horizontal mode, vertical mode, draw_center, panel size as a fraction of its cell)
42CELLS = (
43 ("small", "stretch", "stretch", True, (0.4, 0.5)),
44 ("wide", "stretch", "stretch", True, (1.0, 0.6)),
45 ("tall", "stretch", "stretch", True, (0.35, 1.0)),
46 ("stretch: one copy smeared", "stretch", "stretch", True, (1.0, 1.0)),
47 ("tile: authored size, last clipped", "tile", "tile", True, (1.0, 1.0)),
48 ("tile_fit: whole copies, resized", "tile_fit", "tile_fit", True, (1.0, 1.0)),
49 ("tile horizontal only", "tile", "stretch", True, (1.0, 1.0)),
50 ("tile vertical only", "stretch", "tile", True, (1.0, 1.0)),
51 ("tile both, draw_center=False", "tile", "tile", False, (1.0, 1.0)),
52)
53
54
55def make_panel(size: int = 64, margin: int = MARGIN) -> np.ndarray:
56 """An RGBA panel with coloured corners, edges and centre, and a mark centred in each."""
57 img = np.empty((size, size, 4), dtype=np.uint8)
58 img[:] = (40, 44, 56, 255)
59 img[:margin] = img[-margin:] = (60, 180, 80, 255)
60 img[:, :margin] = img[:, -margin:] = (60, 100, 220, 255)
61 for ys in (slice(None, margin), slice(-margin, None)):
62 for xs in (slice(None, margin), slice(-margin, None)):
63 img[ys, xs] = (220, 80, 60, 255)
64 mid = slice(size // 2 - 4, size // 2 + 4)
65 img[:margin, mid] = img[-margin:, mid] = img[mid, :margin] = img[mid, -margin:] = (255, 255, 255, 255)
66 img[mid, mid] = (200, 200, 90, 255)
67 return img
68
69
70class NinePatchScene(Node2D):
71 def on_ready(self):
72 panel = make_panel()
73 self.title = self.add_child(Text2D(text="NinePatchRect: 9-slice scaling", font_scale=1.5, fit_to_width=True))
74 self.cells = []
75 for caption, horizontal, vertical, centre, fraction in CELLS:
76 label = self.add_child(Text2D(text=caption, fit_to_width=True))
77 patch = self.add_child(
78 NinePatchRect(
79 texture=panel,
80 patch_margin_left=MARGIN,
81 patch_margin_top=MARGIN,
82 patch_margin_right=MARGIN,
83 patch_margin_bottom=MARGIN,
84 axis_stretch_horizontal=horizontal,
85 axis_stretch_vertical=vertical,
86 draw_center=centre,
87 )
88 )
89 self.cells.append((label, patch, fraction))
90
91 def on_update(self, dt):
92 width, height = self.viewport_size
93 top = 50
94 cell_w, cell_h = (width - PAD) / 3, (height - top) / 3
95 self.title.position = (PAD, 12)
96 self.title.rect = (0, 0, width - 2 * PAD, 30)
97 for index, (label, patch, (fw, fh)) in enumerate(self.cells):
98 x, y = PAD + index % 3 * cell_w, top + index // 3 * cell_h
99 label.position = (x, y)
100 label.rect = (0, 0, cell_w - PAD, 20)
101 patch.position = (x, y + 24)
102 patch.size = ((cell_w - PAD) * fw, (cell_h - 24 - PAD) * fh)
103
104
105if __name__ == "__main__":
106 App(width=900, height=600, title="SimVX NinePatchRect").run(NinePatchScene())