A sprite is a coloured box until it is given a picture. The picture is a texture asset,
and the sprite points at it through the ordinary property texture — which is the whole
design: because it is an ordinary property, instance overrides, Apply to container,
Reset to asset, undo and Properties work on it without a single line written for them.
One coin instance can wear a different picture than its nine siblings, and swapping a texture
at runtime is a graph node that already exists.
The Texture row offers pictures only: every texture, each followed by its frames. An
animation is played, not worn (see Animations), so it is not in the list. A
project from before that change may still hold an animation id in the field; it reads as
<id>? like any other dangling reference and draws the plain colour.
Getting an image in. Dropping image files onto the canvas or Ctrl+V pasting one uploads it and makes the asset in one go; several files at once are imported one after another, and a file that fails does not stop the rest.
+ → Texture asks a different question and so opens a different dialog: not the machine's file system, but every picture the server already holds, as a grid of thumbnails. Files are never collected (below), so a picture one project dropped is still there for the next to pick up, and uploading the same bytes twice buys nothing. Picking one and pressing Use image makes a texture asset on that file; Upload… is the way in for a picture the server does not have yet, and lands in the same import as a drop. A tile captioned with an asset name is one this project already wears — picking it selects that asset rather than making a second one.
Tiles are captioned by name, not by hash. A picture this project wears shows its asset name;
any other shows the name the file was uploaded under, which the server keeps beside it (see the
API). Picking such a file names the new asset after it — hero-idle.png becomes
hero-idle, exactly as dropping the file would have — instead of the old Texture 2. Only a
file from before names were kept still reads as its pixel size.
Where it goes. The image is uploaded as raw bytes to POST ?r=textures and stored as
data/textures/{folder}/{sha256}.{ext} — in the uploading account's own folder (#344, see
API), and named after its own content. That name buys
four things: the same picture uploaded twice is one file, a stored file can be cached forever
(a name cannot come to mean different bytes), the project document holds a short string
instead of megabytes of base64, and deleting a texture asset is undoable for free because
nothing on disk is removed. The price is that files outlive the projects that referenced them.
Deleting a file. The picker shows your own files and the ones that belong to nobody; an administrator sees every file, and the line under the list says whose a file is. Delete in the picker removes the picked file from the server: the first press only asks (Delete for good), the second deletes. It is offered for your own files (an administrator: for every file), and the server refuses while a project, a snapshot or a release of any account still uses the file — the line under the list then says which of your projects it is. Fonts and sounds work the same way.
Colour tints the picture. color multiplies the texture, so it defaults to white
(= no tint) and a picture shows its own colours. Tinting is worth having: one white coin
picture becomes ten differently coloured coins, per instance, for nothing. Without a texture
color is simply the fill of the box, as before.
filter is the texture's only property: Linear (default) or Nearest, which is the
difference between mush and crisp when a small image is scaled up. Switching it needs a
style.update() on the texture source, not just the assignment: Pixi's scaleMode setter
writes the filters but leaves the style's cached resource id in place, and that id is the key
the GPU sampler is looked up under — so without it the picture only changes on a reload.
Frames — cutting a sheet up
The Texture tab shows one texture with its frames on top: named rectangles in image
pixels. Drag on empty picture to cut one, drag it or its eight handles to adjust it, and edit
the exact numbers in the inspector; Delete removes the selected frame. Slice… cuts a
regular grid at once — you give columns and rows (not a cell size: everyone knows how many
tiles their sheet has, and deriving the cell size from the picture cannot disagree with it),
with optional offset and spacing for padded sheets, and the dialog says how many frames of
what size the numbers would produce before it commits.
A sprite then wears a frame instead of the whole picture — but through the same
property. texture holds either a texture id (the whole image) or a frame id, because frame
ids come from the same generator and are unique across the document. That is what keeps one
picker, one property and one set of instance overrides, and what makes every sprite written
before frames existed still mean exactly what it meant. The picker lists each sheet followed
by its frames as "Sheet / Frame", and it is searchable, because one sliced sheet adds an entry
per cell.
Frames are intrinsic data on the texture, like a scene's objects and a behavior's graph — not properties, since nothing about them is overridable per instance. All edits go through commands, so cutting, dragging, slicing and re-slicing are undoable, and one drag is one undo step. Deleting a frame leaves the sprites pointing at it: a dangling reference draws the 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.
Collision outline
A frame can carry outlines a tile made of it blocks with (#141; see Tilemaps), and a collider of the shape Frame collision too (#193; see Colliders). Several, because an autotile's block holds several shapes — its bordered tile and its notches. Switch Collision on in the Texture tab's header: a press then edits the selected frame's outlines instead of the frames.
- Click inside the frame to add a corner after the last one of the active outline — that is how an outline is drawn from nothing. The active one is drawn heavier; on a frame just selected it is the last outline.
- Ctrl+click (Cmd+click) starts a new outline there.
- Click a corner to pick it (drawn filled) and drag it to move it. The corner put down last is picked, too.
- Double-click an edge to put a corner there; it is picked and stays in place until it is pressed again. A single click on an edge only makes that outline the active one and drops the pick, and so does a click inside another finished outline.
- Delete takes the picked corner away. Without a picked corner it removes the active outline whole — in collision mode never the frame. So to remove an outline, click one of its edges first.
- A right-click on a corner opens a menu with Delete point.
Corners land on whole pixels inside the frame and are stored relative to its top-left corner, so they move with the frame. Every step is one undo step. The outlines of all frames are always shown; only those of the selected frame have their area tinted, so the tint does not lie over the art of every other frame. One that crosses itself is drawn red and collides as none.
Generate collision…
Instead of clicking corners, Generate collision… in the header reads the outline off the picture (#161). The dialog asks for:
| Row | Default | Meaning |
|---|---|---|
| Max points | 8 | The most corners one outline keeps (3 … 64). |
| Solid from … % alpha | 50 | A pixel at least this opaque is solid. |
| All frames of the texture | off | Every frame of the sheet instead of the selected one. Always on when no frame is selected. |
- Every island of solid pixels gets an outline of its own. Pixels that touch at a corner only are two islands. An island below 1 % of the frame's area is a crumb and gets none, unless it is the largest one there is.
- Holes are not traced: an outline blocks with everything it encloses.
- The outline walks the pixel edges and is then cut down to the corners that matter most. A slope becomes one straight edge, not a staircase. An outline never crosses itself; where the picture would make it, the island's convex hull is taken.
- The line under the rows says what Generate will write, while the numbers change
(
Frame "Left": 1 outline, 3 corners). With nothing solid, the button stays off. - The frames' existing outlines are replaced. It is one undo step, for one frame or all.
When the project is loaded, corners that are no numbers fall away, and an outline with fewer than three corners left is none. A frame saved with a single outline, before a frame could hold several, loads as a frame with that one outline.
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: Repeat tiles them, Stretch
pulls them. Repeat is the default, so only a Stretch is written into the saved document.
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.
Nine-slice scale is a property of the texture, not of a frame: it says how many sprite units
one border pixel is drawn with, for every sliced frame the texture has. The default 1 draws a 16 px
border 16 units wide; at 2 the same border is 32 units, at 0.5 it is 8. The pixels a corner shows
stay the same — only its size on screen changes — so a pixel-art button can have chunky corners
without redrawing the sheet at twice the size. With Repeat the tiles scale along, because a border
drawn twice as big beside a pattern at its old size reads as two different pictures. The rule for a
sprite narrower than its borders applies to the scaled size: two 32-unit borders on a 20-unit box
become two of 10.
Drawings are sliced the same way (#308). Pixel art and vector art carry the same borders, fill and sort line, set on the asset as a whole and measured in canvas pixels, and they have a Nine-slice scale row of their own. It is the same rule and the same inspector rows, only the target differs.
Sort line — where a figure stands
On a tilemap layer sorted by Y (see Tilemaps) a sprite is sorted by the bottom edge of its box. A figure with a shadow or a few empty pixels below its feet would then stand too low. A frame's Sort line moves that point up: whole image pixels from the frame's bottom edge, 0 by default, at most the frame's height. A sprite wearing the frame stands that share of its box higher up, so a line 6 px up a 24 px frame lifts a 48-unit sprite by 12 units.
- It lives on the frame, not on the texture, because one texture can hold several sheets with figures of different sizes. The frames of one walk cycle want the same value, or the figure changes its place in the order while it walks.
- In the Texture tab the selected frame shows it as a dashed yellow line with a knob left of the frame. At 0 the line lies on the bottom edge together with the bottom border and a resize handle, so grab it by the knob; a line inside the frame can also be dragged directly. The inspector has the same number under Sorting. One drag or one typed value is one undo step.
- 0 carries no key in the saved document. It only matters on a Z with a layer sorted by Y.
- Pixel art and vector art have the same row on the asset (#308). A drawing without frames is measured against its whole canvas.
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 to 64 x 48 px puts both sides at the picture's own pixel size. Like ↺ and ↑ the button exists only when it would do something, and follows the same rule for how many: no offer means no button, one is a direct click, both make it the menu form ↔ ▾. So a sprite without a picture, or one already in proportion and at pixel size, carries nothing. Either action is an ordinary property write, so on an object it becomes an override, and setting both sides is still one undo step. The proportions are compared with a relative tolerance rather than exactly — of the 40 000 picture sizes up to 200 x 200, 3135 do not survive the round trip through a division, and an exact test would leave the button standing after its own click. The pixel size is compared exactly, because those two numbers are written as they are.
A sprite straight from a texture. Right-click a texture in the asset list → Create sprite
from texture makes a sprite asset that wears the whole picture at its pixel size, named after the
texture (numbered like any name clash, so Coin gives Coin 2) and selected. It is the same as creating a sprite, picking the texture and clicking Set to
W x H px, but in one undo step.
A frame is a texture of its own. The first time a sprite wears it, the editor cuts the rectangle out of the sheet and uploads that cut. Drawn straight from the sheet, a frame showed a hairline of the neighbouring cell along any edge that landed inside a screen pixel — the GPU evaluates a partly covered pixel at its centre, past the edge — and Pixi lets a fill wrap around the picture, so the top row of a sheet read its bottom row. A cut has no neighbour to show, and sheets need no padding between cells. The price is one small texture per frame on screen, next to the sheet itself.
Nothing ever breaks on a missing picture. While an image loads, and for a reference that dangles because the texture was deleted, the sprite draws its plain colour — exactly what it looked like before textures existed. So there is no placeholder, no flicker and no error path.
Every picture is loaded ahead. As soon as a project is opened, and whenever an asset is added, the editor fetches every image, font and pixel art cel the project names, used or not. Nothing waits for its first draw: an animation or an animated tile that reaches a frame from a picture of its own would otherwise blink out once, the first time it gets there. Deleting a texture warns how many sprites reference it (a texture is never placed, so counting placed instances would report "unused" for a picture a dozen sprites are showing).
A picture can be drawn instead of uploaded
Not every picture is a file: an asset may draw its own. Two do, Vector Art (#129) and Pixel Art (#130). Such an asset is worn exactly like a texture — it stands in the same picker, a sprite names it in its Texture property, a tile can be built from it, and a new sprite is measured by its canvas. What the picture path asks of it is two things: the size of its canvas, and a revision that changes whenever the drawing changes. The revision is what makes a changed drawing a new picture, so nothing anywhere has to ask whether a picture is still current; the editor draws the new revision and lets the old one go.
Two differences to an uploaded picture. A vector drawing has no frames — frames are rectangles
cut out of a sheet, and it has none to cut from. Pixel art does: all its frames are drawn side by
side, and a frame's id reads as a cut of that picture, like the frame of a sliced sheet. And the
picture is not stored: nothing of it lies in data/textures, and it is drawn again after a
reload — from the shapes in the project file, or from the pixel art's cels in data/pixels. The
whole path is the same one an uploaded file takes, which is why a drawn picture works in shaders,
particles, tiles and tilemaps without any of them knowing about it.
A vector drawing is rasterised at twice the resolution of its canvas, so zooming into it does not turn to mush at once; what a sprite is measured by stays the canvas size. The canvas is also the frame: whatever a shape reaches past its edge is not in the picture, which is what makes the size a sprite shows the size one sees.
Limits. nginx caps request bodies at 1 MB, so the editor shrinks images over 2048 px per side and re-encodes to WebP when they are still too big — but an image that already fits is uploaded byte for byte, because re-encoding pixel art would destroy it. Anything still too large is refused with a clear message instead of a broken upload. The server accepts PNG, JPEG, WebP and GIF, deriving the type from the content rather than from anything the client says, and rejects images over 8192 px per side.
Using the editor
- Concepts
- Accounts
- Agents (MCP)
- Scenes
- Scene editing
- Behaviors
- Structs
- Colliders
- Physics
- Textures
- Vector art
- Pixel art
- Fonts
- Videos
- Sounds
- Instruments
- Patterns
- Songs
- Animations
- Curves
- Tiles
- Tilemaps
- State machines
- Shaders
- Particles
- Containers
- Compositions
- Keyboard
Working on the code