NinePatchRect

9-slice panel scaling.

▶ Run in browser

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