NinePatchRect¶
9-slice panel scaling.
▶ Run in browserTags: 2d
Generates a test panel texture with distinct corners, edges, and centre, then renders NinePatchRect nodes at various sizes to demonstrate that:
Corners maintain their original pixel size
Edges stretch in one direction only
Centre fills the remaining space
Shows the engine’s in-memory texture API: the texture property on
NinePatchRect accepts an RGBA uint8 numpy.ndarray directly, no file
I/O required.
--scene tiling runs a second scene comparing the three values of
axis_stretch_horizontal / axis_stretch_vertical on a patterned border:
stretch smears one copy of the artwork across the span, tile repeats it
at its authored pixel size and clips the last repetition, and tile_fit
repeats a whole number of times, resizing each repetition to divide the span
exactly.
Source¶
1"""NinePatchRect: 9-slice panel scaling.
2
3# /// simvx
4# web = { width = 900, height = 600 }
5# ///
6
7Generates a test panel texture with distinct corners, edges, and centre,
8then renders NinePatchRect nodes at various sizes to demonstrate that:
9 - Corners maintain their original pixel size
10 - Edges stretch in one direction only
11 - Centre fills the remaining space
12
13Shows the engine's in-memory texture API: the ``texture`` property on
14NinePatchRect accepts an RGBA uint8 ``numpy.ndarray`` directly, no file
15I/O required.
16
17``--scene tiling`` runs a second scene comparing the three values of
18``axis_stretch_horizontal`` / ``axis_stretch_vertical`` on a patterned border:
19``stretch`` smears one copy of the artwork across the span, ``tile`` repeats it
20at its authored pixel size and clips the last repetition, and ``tile_fit``
21repeats a whole number of times, resizing each repetition to divide the span
22exactly.
23"""
24
25import argparse
26
27import numpy as np
28
29from simvx.core import NinePatchRect, Node2D, Text2D, Vec2
30from simvx.graphics import App
31
32
33def _make_ninepatch_panel(size: int = 64, margin: int = 16) -> np.ndarray:
34 """Generate a panel texture with visually distinct 9-slice regions.
35
36 Corners are bright red, edges are green (horizontal) / blue (vertical),
37 and the centre is a dark grey. A 1px border outlines the whole texture.
38 """
39 img = np.zeros((size, size, 4), dtype=np.uint8)
40
41 for y in range(size):
42 for x in range(size):
43 in_left = x < margin
44 in_right = x >= size - margin
45 in_top = y < margin
46 in_bottom = y >= size - margin
47
48 if (in_top or in_bottom) and (in_left or in_right):
49 # Corners -- bright red/orange
50 img[y, x] = [220, 80, 60, 255]
51 elif in_top or in_bottom:
52 # Horizontal edges -- green
53 img[y, x] = [60, 180, 80, 255]
54 elif in_left or in_right:
55 # Vertical edges -- blue
56 img[y, x] = [60, 100, 220, 255]
57 else:
58 # Centre -- dark grey
59 img[y, x] = [80, 80, 90, 255]
60
61 # 1px border
62 img[0, :] = [255, 255, 255, 255]
63 img[-1, :] = [255, 255, 255, 255]
64 img[:, 0] = [255, 255, 255, 255]
65 img[:, -1] = [255, 255, 255, 255]
66 return img
67
68
69# ---------------------------------------------------------------------------
70# Scene
71# ---------------------------------------------------------------------------
72
73
74class NinePatchScene(Node2D):
75 """Root scene displaying NinePatchRect nodes at different sizes."""
76
77 def on_ready(self):
78 margin = 16
79
80 # Generate the panel pixels in memory and hand the ndarray directly to
81 # NinePatchRect.texture: the renderer uploads it via
82 # TextureManager.resolve() / load_from_array().
83 panel = _make_ninepatch_panel(64, margin)
84
85 # Small -- just larger than the margins
86 self.add_child(
87 NinePatchRect(
88 texture=panel,
89 size=(80, 60),
90 patch_margin_left=margin,
91 patch_margin_right=margin,
92 patch_margin_top=margin,
93 patch_margin_bottom=margin,
94 position=Vec2(40, 60),
95 name="Small",
96 )
97 )
98
99 # Medium -- typical button/panel size
100 self.add_child(
101 NinePatchRect(
102 texture=panel,
103 size=(250, 100),
104 patch_margin_left=margin,
105 patch_margin_right=margin,
106 patch_margin_top=margin,
107 patch_margin_bottom=margin,
108 position=Vec2(40, 160),
109 name="Medium",
110 )
111 )
112
113 # Large -- wide dialogue box
114 self.add_child(
115 NinePatchRect(
116 texture=panel,
117 size=(500, 200),
118 patch_margin_left=margin,
119 patch_margin_right=margin,
120 patch_margin_top=margin,
121 patch_margin_bottom=margin,
122 position=Vec2(40, 300),
123 name="Large",
124 )
125 )
126
127 # Tall narrow panel
128 self.add_child(
129 NinePatchRect(
130 texture=panel,
131 size=(80, 250),
132 patch_margin_left=margin,
133 patch_margin_right=margin,
134 patch_margin_top=margin,
135 patch_margin_bottom=margin,
136 position=Vec2(580, 60),
137 name="Tall",
138 )
139 )
140
141 # Labels
142 self.add_child(
143 Text2D(text="NinePatchRect Demo -- 9-Slice Scaling", position=(10, 10), font_scale=1.5, name="Title")
144 )
145 self.add_child(Text2D(text="Small (80x60)", position=(140, 75), name="LabelSmall"))
146 self.add_child(Text2D(text="Medium (250x100)", position=(300, 195), name="LabelMed"))
147 self.add_child(Text2D(text="Large (500x200)", position=(300, 385), name="LabelLarge"))
148 self.add_child(Text2D(text="Tall (80x250)", position=(580, 330), name="LabelTall"))
149
150
151def _make_patterned_panel(size: int = 64, margin: int = 16) -> np.ndarray:
152 """A panel whose borders carry a motif, so repeating it is visible.
153
154 The plain panel above has flat edge bands, and a flat band stretched looks
155 exactly like the same band tiled. Each edge here carries one bright dash in
156 the middle of its 32 px span, which a stretched span smears into one long
157 bar and a tiled span reproduces once per repetition.
158 """
159 img = np.zeros((size, size, 4), dtype=np.uint8)
160 inner = size - margin
161 img[:] = [40, 44, 56, 255]
162 img[:margin, :] = img[inner:, :] = [60, 180, 80, 255]
163 img[:, :margin] = img[:, inner:] = [60, 100, 220, 255]
164 # Corners last, so they win over both edge bands.
165 for ys in (slice(0, margin), slice(inner, size)):
166 for xs in (slice(0, margin), slice(inner, size)):
167 img[ys, xs] = [220, 80, 60, 255]
168
169 # One dash per edge band and one square in the centre, each centred in its
170 # span so a repetition is unambiguous to count.
171 mid = slice(size // 2 - 4, size // 2 + 4)
172 img[:margin, mid] = img[inner:, mid] = [255, 255, 255, 255]
173 img[mid, :margin] = img[mid, inner:] = [255, 255, 255, 255]
174 img[mid, mid] = [200, 200, 90, 255]
175 return img
176
177
178class TilingScene(Node2D):
179 """The same patterned panel at one size under each axis stretch mode.
180
181 Every panel is 200x150 with 16 px margins on a 64x64 source, so each edge
182 band has 32 source px to cover 168 horizontally and 118 vertically: 5.25 and
183 3.69 repetitions, neither of them whole. That is what separates the modes.
184 """
185
186 #: ``(mode, x, caption)``. Three panels side by side at one size, so the
187 #: only difference between them is the mode.
188 COLUMNS = (
189 ("stretch", 20, "one copy smeared"),
190 ("tile", 260, "5 whole + 1 clipped"),
191 ("tile_fit", 500, "5 equal, resized"),
192 )
193
194 #: ``(horizontal, vertical, x, label)``. The second row sets one axis at a
195 #: time, which is what shows that each mode governs its own three regions.
196 AXES = (
197 ("tile", "stretch", 20, "horizontal: top, bottom, centre"),
198 ("stretch", "tile", 260, "vertical: sides, centre"),
199 ("tile", "tile", 500, "both, draw_center=False"),
200 )
201
202 def on_ready(self):
203 margin = 16
204 panel = _make_patterned_panel(64, margin)
205
206 self.add_child(
207 Text2D(text="NinePatchRect -- axis stretch modes", position=(20, 15), font_scale=1.5, name="Title")
208 )
209 self.add_child(
210 Text2D(
211 text="Each panel is 200x150 from the same 64x64 source with 16 px margins.",
212 position=(20, 48),
213 name="Caption",
214 )
215 )
216 for mode, x, caption in self.COLUMNS:
217 self.add_child(Text2D(text=mode, position=(x, 90), font_scale=1.2, name=f"Label{mode}"))
218 self.add_child(Text2D(text=caption, position=(x, 118), name=f"Caption{mode}"))
219 self.add_child(
220 NinePatchRect(
221 texture=panel,
222 size=(200, 150),
223 patch_margin_left=margin,
224 patch_margin_right=margin,
225 patch_margin_top=margin,
226 patch_margin_bottom=margin,
227 axis_stretch_horizontal=mode,
228 axis_stretch_vertical=mode,
229 position=Vec2(x, 150),
230 name=f"Panel{mode}",
231 )
232 )
233
234 self.add_child(
235 Text2D(
236 text="Set one axis at a time: a mode governs only the three regions that grow along it.",
237 position=(20, 350),
238 name="AxesCaption",
239 )
240 )
241 for index, (horizontal, vertical, x, label) in enumerate(self.AXES):
242 self.add_child(Text2D(text=label, position=(x, 378), name=f"LabelAxes{index}"))
243 self.add_child(
244 NinePatchRect(
245 texture=panel,
246 size=(200, 150),
247 patch_margin_left=margin,
248 patch_margin_right=margin,
249 patch_margin_top=margin,
250 patch_margin_bottom=margin,
251 axis_stretch_horizontal=horizontal,
252 axis_stretch_vertical=vertical,
253 # The last panel drops the centre, which is how a nine-patch
254 # draws a frame over content it must not cover.
255 draw_center=index < len(self.AXES) - 1,
256 position=Vec2(x, 410),
257 name=f"PanelAxes{index}",
258 )
259 )
260
261
262SCENES = {"panels": NinePatchScene, "tiling": TilingScene}
263
264
265if __name__ == "__main__":
266 parser = argparse.ArgumentParser(description="NinePatchRect reference scenes")
267 parser.add_argument("--scene", choices=tuple(SCENES), default="panels", help="which nine-patch scene to run")
268 name = parser.parse_args().scene
269 app = App(width=900, height=600, title=f"SimVX NinePatchRect Demo ({name})")
270 app.run(SCENES[name]())