NinePatch¶
9-slice sprite scaling via Draw2D.draw_texture_region().
▶ 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.
Source¶
1"""NinePatch: 9-slice sprite scaling via Draw2D.draw_texture_region().
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
18
19import sys
20
21import numpy as np
22
23from simvx.core import NinePatchRect, Node2D, Text2D
24from simvx.core.math.types import Vec2
25from simvx.graphics import App
26
27
28def _make_ninepatch_panel(size: int = 64, margin: int = 16) -> np.ndarray:
29 """Generate a panel texture with visually distinct 9-slice regions.
30
31 Corners are bright red, edges are green (horizontal) / blue (vertical),
32 and the centre is a dark grey. A 1px border outlines the whole texture.
33 """
34 img = np.zeros((size, size, 4), dtype=np.uint8)
35
36 for y in range(size):
37 for x in range(size):
38 in_left = x < margin
39 in_right = x >= size - margin
40 in_top = y < margin
41 in_bottom = y >= size - margin
42
43 if (in_top or in_bottom) and (in_left or in_right):
44 # Corners -- bright red/orange
45 img[y, x] = [220, 80, 60, 255]
46 elif in_top or in_bottom:
47 # Horizontal edges -- green
48 img[y, x] = [60, 180, 80, 255]
49 elif in_left or in_right:
50 # Vertical edges -- blue
51 img[y, x] = [60, 100, 220, 255]
52 else:
53 # Centre -- dark grey
54 img[y, x] = [80, 80, 90, 255]
55
56 # 1px border
57 img[0, :] = [255, 255, 255, 255]
58 img[-1, :] = [255, 255, 255, 255]
59 img[:, 0] = [255, 255, 255, 255]
60 img[:, -1] = [255, 255, 255, 255]
61 return img
62
63
64# ---------------------------------------------------------------------------
65# Scene
66# ---------------------------------------------------------------------------
67
68class NinePatchScene(Node2D):
69 """Root scene displaying NinePatchRect nodes at different sizes."""
70
71 def on_ready(self):
72 margin = 16
73
74 # Generate the panel pixels in memory and hand the ndarray directly to
75 # NinePatchRect.texture: the renderer uploads it via
76 # TextureManager.resolve() / load_from_array().
77 panel = _make_ninepatch_panel(64, margin)
78
79 # Small -- just larger than the margins
80 self.add_child(NinePatchRect(
81 texture=panel,
82 size=(80, 60),
83 patch_margin_left=margin, patch_margin_right=margin,
84 patch_margin_top=margin, patch_margin_bottom=margin,
85 position=Vec2(40, 60), name="Small",
86 ))
87
88 # Medium -- typical button/panel size
89 self.add_child(NinePatchRect(
90 texture=panel,
91 size=(250, 100),
92 patch_margin_left=margin, patch_margin_right=margin,
93 patch_margin_top=margin, patch_margin_bottom=margin,
94 position=Vec2(40, 160), name="Medium",
95 ))
96
97 # Large -- wide dialogue box
98 self.add_child(NinePatchRect(
99 texture=panel,
100 size=(500, 200),
101 patch_margin_left=margin, patch_margin_right=margin,
102 patch_margin_top=margin, patch_margin_bottom=margin,
103 position=Vec2(40, 300), name="Large",
104 ))
105
106 # Tall narrow panel
107 self.add_child(NinePatchRect(
108 texture=panel,
109 size=(80, 250),
110 patch_margin_left=margin, patch_margin_right=margin,
111 patch_margin_top=margin, patch_margin_bottom=margin,
112 position=Vec2(580, 60), name="Tall",
113 ))
114
115 # Labels
116 self.add_child(Text2D(text="NinePatchRect Demo -- 9-Slice Scaling", x=10, y=10, font_scale=1.5, name="Title"))
117 self.add_child(Text2D(text="Small (80x60)", x=140, y=75, name="LabelSmall"))
118 self.add_child(Text2D(text="Medium (250x100)", x=300, y=195, name="LabelMed"))
119 self.add_child(Text2D(text="Large (500x200)", x=300, y=385, name="LabelLarge"))
120 self.add_child(Text2D(text="Tall (80x250)", x=580, y=330, name="LabelTall"))
121
122
123if __name__ == "__main__":
124 test_mode = "--test" in sys.argv
125 app = App(width=900, height=600, title="SimVX NinePatchRect Demo", visible=not test_mode)
126 scene = NinePatchScene()
127
128 if test_mode:
129 frames = app.run_headless(scene, frames=5, capture_frames=[4])
130 if frames and frames[0].max() > 0:
131 print(f"PASS: frame {frames[0].shape} is not blank")
132 else:
133 print("FAIL: frame is blank or missing")
134 else:
135 app.run(scene)