NinePatch

9-slice sprite scaling via Draw2D.draw_texture_region().

▶ Run in browser

Tags: 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)