9-Slice Editor — Nine-Patch Borders for Godot and Unity
Borders are four numbers in source pixels. The editor suggests them from the longest run of identical columns and rows, lets you drag, type or nudge them, and renders every target size with corners that never change.
A nine-slice (9-patch) border is four numbers in source pixels — left, right, top, bottom — that cut an image into a 3 × 3 grid. The four corners are always drawn at their own size, the top and bottom edges stretch or repeat horizontally, the left and right edges vertically, and the centre both ways. Set the borders once here (suggested from identical columns and rows), preview any size, then enter the same numbers in Godot 4 (NinePatchRect.patch_margin_*), Unity (Sprite Editor border, Image Type Sliced) or Phaser (add.nineslice). The download is PNG + nine-slice.json + SETUP.md; rendering inside the engines was not run.

- Nine-sliceMeasured· Pillow
- Bitmap fontMeasured· independent .fnt parser
- Button statesMeasured· Pillow
- Engine importUNVERIFIED
The nine regions and what each one does
For a W × H image with borders L, R, T and B the cuts give four corners (L × T, R × T, L × B, R × B), a top and a bottom edge (W − L − R wide), a left and a right edge (H − T − B tall) and the centre. When the panel is resized, corners are copied unscaled, horizontal edges change only their width, vertical edges only their height and the centre both. That is why a rounded or bevelled corner survives any size, and why the smallest size that shows every corner whole is (L + R) × (T + B).
Stretch resamples an edge or the centre to the new length, which is right for flat colour or a gradient along the other axis. Tile repeats it at its natural size, which keeps a pattern (rivets, stitches, a brick edge) undistorted; Nerulio clips the last partial tile. Godot 4 also has Tile Fit, which stretches the tiles slightly so each one is shown whole; Nerulio's preview does not draw that mode.
Every engine stores the same four numbers under different names and in a different order. Godot 4 uses patch_margin_left/right/top/bottom on a NinePatchRect (integers, in pixels) and texture_margin_* on a StyleBoxTexture. Unity's Sprite Editor has L, R, T and B fields, while the scripting value Sprite.border is a Vector4 in the order left, bottom, right, top. Phaser's NineSlice takes leftWidth, rightWidth, topHeight, bottomHeight after the size, and becomes a 3-slice when only the left and right widths are given.
- Border (patch margin)
- The distance in source pixels from one edge of the image to the cut line.
- Stretch
- An edge or the centre is scaled to fill the new length.
- Tile
- An edge or the centre is repeated at its original size.
- Minimum size
- (L + R) × (T + B): below it the corners cannot be drawn whole and are squashed.
Example: a 48 × 48 panel drawn at 300 × 80
Borders 12 px on every side. These are the rectangles the editor's draw plan produces, the same plan the exported previews use:
source 48 × 48 borders L 12 · R 12 · T 12 · B 12 target 300 × 80 stretch
corners 4 × (12×12 → 12×12) copied 1:1, never resampled
top/bottom 24×12 → 276×12 276 = 300 − 12 − 12 (×11.5 horizontally only)
left/right 12×24 → 12×56 56 = 80 − 12 − 12 (×2.33 vertically only)
centre 24×24 → 276×56
tile mode top edge = 11 tiles of 24 px + 1 clipped tile of 12 px (11 × 24 + 12 = 276)
too small target 20 × 80: 20 < 12 + 12 → corners squashed to 10 + 10 (warning "narrow")
asymmetric L 10 · R 6 · T 8 · B 14 → Unity Sprite.border (x, y, z, w) = (L, B, R, T) = (10, 14, 6, 8)
Godot patch_margin_left 12 · right 12 · top 12 · bottom 12
Phaser this.add.nineslice(x, y, "panel", null, 300, 80, 12, 12, 12, 12)With an integer scale of 2 the corners are drawn at exactly 24 × 24 (each source pixel becomes 2 × 2), so a pixel-art frame stays crisp at double size.
What it does
- Border suggestion from identical columns/rows, shown with the run it came from (never applied silently).
- Stretch or tile edges; tiles repeat at their natural size and the last tile is clipped.
- Corners are never scaled (checked at 100×40, 300×80, 800×200, 420×120): the same bytes in Chromium; in Firefox semi-transparent corner pixels can shift by one alpha-rounding step.
- Export: previews, nine-slice.json with pixels, normalised values, Godot patch_margin_* and Unity border (L, B, R, T), and SETUP.md.
How to do it
- Drop the panel or button PNG.
- Accept the suggested borders or drag/type them (arrow keys nudge 1 px, Shift 10 px).
- Preview at your target sizes, stretch or tile.
- Export the ZIP.
Files you get
| File | What it is for |
|---|---|
panel.png | The source image, unchanged, named after your file. |
previews/100x40.png, 300x80.png, 800x200.png + custom size | The panel rendered at each preview size with your mode and scale (the custom size defaults to 420 × 120). |
nine-slice.json | The borders as pixels, normalized (border ÷ width or height), godot4 (patch_margin_*) and unity (border in L, B, R, T order, named), plus mode, scale and the preview sizes. |
SETUP.md | Short Godot 4 and Unity steps with your numbers filled in, marked as not run inside the engines. |
Enter the borders in Godot, Unity or Phaser
- Godot 4: add a
NinePatchRect, set its Texture to the PNG andpatch_margin_left,patch_margin_right,patch_margin_top,patch_margin_bottomto the four numbers fromnine-slice.json. - Godot 4, tile mode: set
axis_stretch_horizontalandaxis_stretch_verticalto Tile (or Tile Fit). For a Button or Panel theme, use aStyleBoxTexturewith the same numbers intexture_margin_*and the same axis stretch settings. - Unity: select the texture, set Mesh Type to Full Rect and click Apply, open the Sprite Editor and drag the green handles or type the L, R, T and B fields (the
pixelsvalues). - Unity: on a Sprite Renderer set Draw Mode to Sliced (or Tiled, with Tile Mode Continuous or Adaptive); on a UI Image set Image Type to Sliced (or Tiled).
- Phaser 3.60 or later:
this.add.nineslice(x, y, key, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight). The Phaser 4.1 API liststileXandtileYto repeat instead of stretch. - For pixel art keep Nearest filtering and resize by whole numbers of source pixels, so the corners stay sharp.
Check the result
- Resize the engine node to 300 × 80 and compare it with
previews/300x80.png: the four 12 × 12 corners should match pixel for pixel. - Shrink it below L + R wide: Nerulio warns "narrow" for the same size, and Phaser documents L + R as the minimum width.
- In Unity,
Debug.Log(sprite.border)should print theunity.bordernumbers from the JSON (left, bottom, right, top).
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| Corners stretch in Unity | Mesh Type is Tight, or the Image or Sprite Renderer still draws Simple | Texture import settings; Image Type or Draw Mode | Mesh Type Full Rect + Apply, then Image Type Sliced or Draw Mode Sliced |
| Top and bottom borders look swapped in Unity | The Vector4 order (left, bottom, right, top) was typed into the L, R, T, B fields, or the other way round | Compare the Sprite Editor fields with pixels in the JSON | Type pixels into the Sprite Editor; use unity.border only for Sprite.border in scripts |
| Corners look squashed at small sizes | The control is smaller than L + R or T + B | Nerulio shows the "narrow" or "short" warning for that size | Make the control larger or the borders smaller |
| A patterned edge looks smeared | The edge is stretched, so a repeating pattern is resampled | Switch the preview to Tile and compare | Godot: axis stretch Tile or Tile Fit; Unity: Tiled; Phaser 4: tileX / tileY |
| Pixel-art corners are blurry | Linear filtering, or a scale that is not a whole number | Zoom in: soft steps on the corner | Nearest filtering and integer scales (the editor's scale is 1–4) |
| Phaser NineSlice from a TexturePacker atlas looks wrong | The frame was trimmed; the Phaser docs say NineSlice does not support trimmed TexturePacker textures | Check the atlas JSON for trimmed frames | Pack the panel untrimmed, or load it as its own image |
| "Suggest borders" finds no clear run | The middle is a gradient or noise, so no columns or rows are identical | The suggestion says there is no repeating column or row | Place the guides by hand where the corner art ends |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Set the borders directly in Godot's NinePatchRect inspector or Unity's Sprite Editor | One or two panels, already in the engine: you see the real engine rendering while you drag. |
| The UI Lab atlas stage | Many panels on one sheet: each element gets its own borders and they all travel in one ui-atlas.json. |
Outputs and how each was checked
"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.
| Target | Files | Check |
|---|---|---|
| Nine-slice | nine-slice.json · previews · SETUP.md | Measured PillowEvery resized preview keeps all four corner blocks unscaled: the same bytes in Chromium; in Firefox semi-transparent pixels can shift by one alpha-rounding step. 0 gaps, 0 overlaps. |
| Bitmap font | font.png · font.fnt (BMFont text) · font.json | Measured independent .fnt parserThe .fnt was read back by a parser written separately in the test: header and every glyph rect match. |
| Button states | states/*.png · strip · states.json | Measured PillowNormal equals the source (byte for byte in Chromium; in Firefox semi-transparent colour within one alpha-rounding step); every rect in states.json cuts exactly its state out of the strip. |
| Engine import | Godot NinePatchRect · Unity Sprite border · BMFont import | UNVERIFIEDNot run in Godot or Unity. |
Evidence
Nine-slice corners unscaled at 100×40, 300×80, 800×200 and 420×120; border suggestion 6·6·6·6 from a run of 12 identical columns; button states: normal equal to the source, disabled grey at alpha 128, focus ring exact; .fnt glyph records equal the JSON; missing glyphs from a .po listed exactly; black on white measured 21:1. Images pass through a browser canvas: Chromium returns the same bytes; Firefox rounds the colour of semi-transparent pixels to one premultiplied step (measured on a Kenney button: up to 4 levels at alpha 63), while alpha and opaque pixels stay exact.
Limits
- Rendering inside Godot, Unity or Phaser was not run: the numbers match their documentation, not an engine test.
- No engine files are written (no
.tres,.metaor Android.9.png). - Tile mode at an integer scale above 1 squashes the final partial tile by up to (scale − 1) ÷ scale of a source pixel.
- No kerning pairs, one font page, no MSDF; the SDF output is Beta.
- A TTF's cmap is not parsed, so the missing-glyph check needs a .fnt or the Lab's own font.
- One atlas page; touching elements count as one.
- Rendering inside Godot or Unity was not run: the engine numbers (patch margins, L-B-R-T border) are documented, not engine-tested.
Versions and sources
Measured in Chromium for docs/UI-LAB.md: the 6 × 6 corner blocks of every exported preview are byte-identical to the source at 100 × 40, 300 × 80, 800 × 200 and 420 × 120; a plan has 0 gaps and 0 overlaps at 300 × 80, at scale 3 and when squashed to 8 × 4; a striped 12 × 12 panel tiles with its 4 px period; the suggestion finds 6 · 6 · 6 · 6 from runs of 12 identical columns and rows. In Firefox semi-transparent corner pixels can shift by one alpha-rounding step. Godot and Unity import is UNVERIFIED. Engine settings above follow Godot 4.7, Unity 6 and the Phaser API documentation.
Official documentation used
Questions
What order does Unity use?
Unity's scripting value Sprite.border is a Vector4 in the order Left, Bottom, Right, Top; the JSON writes that order and names it. The Sprite Editor itself shows the same numbers as L, R, T and B fields, so type the pixel values there.
Was it tested inside Godot or Unity?
No. The numbers are the ones those engines document; rendering in the engines was not run.
Is it free, and do I need an account?
Everything on these pages is free and works without an account. There is no watermark and no export that is held back.
Are my sprites uploaded?
No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.
Does it need Aseprite or an engine plugin installed?
No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).