Nine-slice: borders and fill on a texture frame (#41)

admin committed 2026-09-16 12:04:12 +02:00
1 parent b0d0e016d3
commit 15f6f33b19
3 files changed
+38 -4

No files matched your search

+3 -3
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 945 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 955 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
@@ -64,7 +64,7 @@ The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, asse
graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect,
container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable,
variable nodes, property nodes, animations, state machines, console, shaders, transforms, fonts,
particles, shared scope, render on change, sounds, load scene, graph groups) that build on each other's
particles, shared scope, render on change, sounds, load scene, graph groups, nine-slice) that build on each other's
scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`,
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
exercises.
@@ -232,4 +232,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 945 checks as here.
WebGL 2.0, and the same 955 checks as here.
+1 -1
@@ -14,7 +14,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Scene editing](Scene-editing) | Placing by drag, editor symbols, z-order, hierarchy and parenting, the editor offset |
| [Behaviors](Behaviors) | Node graphs: variables and properties, Variables assets, the node set, structs and lists, loops, behaviors as nodes, editing the graph, the live view |
| [Colliders](Colliders) | Collider assets, collision events, blocking movement |
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, upload limits |
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, nine-slice, upload limits |
| [Fonts](Fonts) | Uploading a typeface, what a text wears, what the editor falls back to |
| [Sounds](Sounds) | Uploading a sound, what the asset says, `Play sound` and `Stop sound`, what a pause and a stop do to a voice |
| [Animations](Animations) | Animation assets, the Animation tab, how they run |
+34
@@ -66,6 +66,40 @@ undo step. Deleting a frame leaves the sprites pointing at it: a dangling refere
plain colour and comes back by itself on undo. Deleting the whole texture counts the sprites
wearing *any* of its frames, not just the ones naming the texture directly.
## Nine-slice — a frame that keeps its corners
A frame carries four **borders** and a **fill**, and a sprite wearing it is drawn as nine parts
instead of one: the four corners keep their own pixel size however big the box gets, the four edges
grow along one axis, and the middle grows along both. That is what makes one 24 x 24 picture serve
every button, panel and dialog in a game without the rounded corner smearing.
The borders live on **the frame**, not on the sprite — set them once on the picture and all ten
buttons wearing it are sliced the same way, and a new version of the artwork is nudged in one
place. The price is that a sprite cannot decide to ignore them: a picture that should sometimes be
stretched whole wants a second frame without borders.
Set them in the **Texture tab**: the selected frame shows four dashed lines, one per border, and
dragging one moves that border alone; the inspector has the same four numbers next to the frame's
rectangle. The lines are tested *after* the frame's own eight resize handles and *before* its body,
so a border still sitting on the frame's edge cannot steal the grip that resizes the frame, and the
whole inside of a frame does not swallow the lines it holds. **All four at zero means the frame is
not sliced at all** — it is drawn over the whole box, exactly as every sprite was before #41, and a
frame nobody sliced carries no border keys in the saved document.
**Fill** says how the parts *between* the corners fill their space: `Stretch` pulls them, `Repeat`
tiles them. One setting for the edges and the middle together, because a brick pattern that tiles
along the top and stretches in the centre reads as a mistake. A tiled part is built as one quad per
tile, each with the frame's own UVs and the last one cut off where the space ends — sampling past
1 with a repeating address mode would pull in the *neighbouring cell of the sheet*, which is the
same trap the frame cut-outs exist to avoid. Above 64 tiles along an axis the part stretches
instead; that is a guard against a buffer that grows with the sprite, not a design.
Two sizes have an answer of their own. A sprite **narrower than its two borders** does not draw
them over each other: both are scaled down together until the middle is exactly zero and the
corners meet at a seam. And a frame can never be given borders that leave *no* middle — they are
squeezed to leave at least one pixel, because a middle of no pixels has nothing to draw the space
between the corners with, and that would show as a hole rather than as an error.
**Fitting the sprite to the picture.** The *Width* and *Height* rows of a sprite carry a ↔ / ↕
button offering two things: **Fit to 64 x 48** sets that one side from the other and the picture's
proportions — the frame's rectangle when it wears a frame, the whole image otherwise — and **Set