Add the Pixel Art page and link it from the other pages (#130)

New page Pixel Art. Sidebar, Home, API, Architecture, Textures, Tiles and Development are updated, the check total is 1259.
admin committed 2026-09-21 17:33:51 +02:00
1 parent a726d4a907
commit d63d71d1a1
8 files changed
+153 -15

No files matched your search

+11 -2
@@ -20,13 +20,14 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
| GET | `sounds` | `[{file,name,bytes,updatedAt}]` — what the sound picker offers |
| POST | `sounds&name=jump.wav` | **raw sound bytes** → `{file,name,bytes}` (201) |
| DELETE | `sounds/{file}` | 204 |
| POST | `pixels` | **raw cel bytes** → `{file,bytes}` (201) — the pixels of one layer on one frame of [pixel art](Pixel-Art); **422** for anything that is not a cel. No listing and no delete: a cel is found by the name in the project |
| GET | `plugins` | `[{file,bytes,updatedAt}]` — every `.js` in `data/plugins/`, sorted by file name |
| GET | `plugins/{file}` | the module source as `text/javascript` (`nosniff`, `no-cache`); 404 for anything but a bare file name there — see [Plugins](Plugins) |
**Everything but `auth/*` needs a session** and answers **401** `{"error":"Sign in first"}` without
one. Projects belong to an account: `projects` lists only the caller's own, and somebody else's
project answers **404**, not 403 — that an id exists is already more than a stranger should learn.
Images, fonts, sounds and plugins are shared between accounts; being signed in is still the price
Images, fonts, sounds, cels and plugins are shared between accounts; being signed in is still the price
of admission. See [Accounts](Accounts).
Which editor plugins a project switches on is the project's own field `plugins` (#119); the server
@@ -46,7 +47,7 @@ own caps behind nginx's: 20 MB per request, 4 MB per image or font and 16 MB per
a single music track is worth more than four typefaces. **nginx's own limit is the one that
actually bites** (1 MB today, see `MAX_UPLOAD_BYTES` in `textureImport.ts`): raising the API's
cap without raising that one changes nothing. The stored files are served statically from
`data/textures/`, `data/fonts/` and `data/sounds/`, so reading one never runs PHP. An image is
`data/textures/`, `data/fonts/`, `data/sounds/` and `data/pixels/`, so reading one never runs PHP. An image is
downscaled and re-encoded in the browser when it would not fit; a font or a sound cannot be, so
an oversized one is refused rather than shrunk.
@@ -55,6 +56,14 @@ The format of a font or a sound is read from the file's **own signature**, never
nothing at all for the same `.m4a`). MP3 is the one with no single signature — either an `ID3`
tag or a frame header of eleven set bits, which is what `SoundStore::mp3Extension` demands.
**A cel** (#130) is `GMPX`, a version byte (1), a reserved byte, width and height as little-endian
16-bit numbers, then one palette index per pixel, row by row; 0 is transparent, a side is 1 to 512
pixels. It is stored as `data/pixels/{sha256}.px`. The editor works the same name out in the browser
before it uploads, so a stroke can name its cel in the project at once; the name the server answers
with is the check that both hashed the same bytes. A pixel art asset in the project document holds
`width`, `height`, `layers` (`{id,name,opacity,hidden?}`) and `frames` (`{id,cels}`, `cels` maps a
layer id to a cel file), a palette asset `colors` (0xRRGGBB, index 1 first).
**`name` is the original file name**, kept beside the stored file as `{file}.json` and returned
with it. The stored file is always `{sha256}.{ext}`, so the name is a caption and never a path:
the server strips directories, control characters and anything not valid UTF-8, and caps it at
+10 -2
@@ -72,10 +72,10 @@
```
web/index.html Vite source page (dev) index.html built page (generated, served)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>` and the register of readers that tell the picture path an asset's canvas size and revision), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>` and the register of readers that tell the picture path an asset's canvas size and revision), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips)
src/core/commands/ undoable commands, CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
src/core/commands/ undoable commands (pixelArtCommands: one command runs any pixel art edit and keeps the state before it, #130), CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
src/core/graph/ graph model, NodeRegistry/NodeContext/values (a travelling list may hold live objects, `storedList`
narrows one down to what a PropertyBag may hold, #157), interpreter, dropTarget, nested (behaviors as nodes),
emitGraph (the walk both graph compilers share: each node once, in dependency order; cycles and property names reported),
@@ -117,6 +117,9 @@ src/viewport/ view (transforms), items (what to draw), symbols (how to
resolution; registers vector art in both halves of the picture path; PaintCache keeps a
painter's gradients and hands the ones it no longer draws to Pixi's texture garbage
collector, and a texture fill waits for its picture instead of drawing without it, #186),
PixelCels (the pixels of pixel art in memory: a painted cel is named at once, the ones the
server lacks are uploaded before a save, #130) + pixelArtPicture (every frame composed on the
CPU and laid side by side onto one canvas, so a frame is cut out like a texture frame),
SoundLibrary, ShaderLibrary, ParticleLibrary,
particles (one ParticleContainer per source), tilemap (one container per layer),
drawOrder (objects and tilemap layers by z, #174), activeCamera — no editor concepts,
@@ -171,6 +174,11 @@ src/editor/graph/ GraphEditor (header, breadcrumb), GraphCanvas, graphView
src/editor/container/ ContainerEditor (header), ContainerCanvas (a SceneViewport on a template)
src/editor/texture/ TextureEditor (header), TextureCanvas (picture + frames), SliceDialog (a FormDialog), GenerateCollisionDialog (#161: the rows, and a summary that traces while the numbers change; the texture library hands it the sheet's pixels), sheetView (the grid maths),
ImagePicker + imageGrid (the "+ → Texture" dialog), checker (the transparency board)
src/editor/pixelart/ PixelArtEditor (header: Resize…), PixelArtCanvas (one frame composed as the picture path composes it,
whole zoom steps, grid, onion skin and tile preview; a stroke paints a copy and becomes one command),
PixelArtSide (tools, palette, layers, frames, view, preview), PaletteGrid, PixelPreview, ResizeCanvasDialog,
pixelActions (what the panel does; merge and resize work out the new cels), pixelView (zoom steps, the pixel
under the pointer), celUploadStore (the project store that uploads the cels before it saves)
src/editor/vectorart/ VectorArtEditor (header: the three tools, the picked point's type or, with Paint, whose handles, and the magnet), VectorArtCanvas (the drawing
painted from core/model/vectorPath onto its own Graphics, with points and handles over it;
the pen puts points down, the arrow drags points, handles and whole shapes, #129; the Paint
+1 -1
@@ -13,7 +13,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
php tools/make-user.php <name> <password> # an account; every tool signs in (see Accounts)
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1232 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1259 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
+1
@@ -18,6 +18,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Physics](Physics) | Rigidbodies, the collider material, gravity and units, the fixed step, the contact on `On collision enter` |
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, nine-slice, upload limits |
| [Vector art](Vector-Art) | The vector art asset, canvas and shapes, the Pen and Select tools, the shape list, how a drawing is worn |
| [Pixel art](Pixel-Art) | The pixel art asset and its palette, the tools, layers and frames, onion skin and the preview, resizing, how frames are worn and where the pixels are stored |
| [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 |
+116
@@ -0,0 +1,116 @@
A **pixel art asset** is a picture you paint pixel by pixel, in **layers** and **frames**, with the
colours of a **palette**. Like [vector art](Vector-Art) it is drawn rather than uploaded, and worn
exactly like a texture: a sprite names it in its *Texture* row, a tile or an animation key can name
one of its frames, a shader samples it. See
[Textures](Textures#a-picture-can-be-drawn-instead-of-uploaded) for the contract behind that.
**Making one.** **+ → Pixel art** creates the asset and opens it in the **Pixel art** tab. It stands
in the assets panel under *Pixel drawings*. The first pixel art of a project brings a **palette**
along in the same undo step; later ones take the palette that is already there. Pixel art is not
placed in a scene, it is worn by something that is.
## The canvas
A new canvas is **32x32** pixels, at most **512x512**. **Resize…** in the tab's header asks for the
new width and height and for the **anchor**: nine buttons for the corners, the edges and the centre.
The picture keeps its place at that anchor, and whatever sticks out on the far side is cut off. A
resize is one undo step, for every layer on every frame at once.
The inspector shows two rows: **Palette** (which palette the picture is painted in) and **Filter**,
**Nearest** by default so the pixels stay sharp when a sprite scales them up.
## Colours and the palette
Every pixel is an **index** into the palette. Index 0 is transparent. A palette is an asset of its
own, so several pictures can share one, and **changing a colour of the palette recolours every pixel
painted with it**, in every picture that uses that palette. Pointing a picture's *Palette* row at
another palette with the same number of colours is a palette swap.
A new palette starts with the sixteen colours of PICO-8. The **COLOURS** block of the tab shows them
as swatches; the first swatch, a checkerboard, is transparent. A click picks the colour to paint with.
| Control | What it does |
|---|---|
| Colour field | Changes the picked colour of the palette, one undo step per change |
| **+ Colour** | Adds a copy of the picked colour at the end and picks it (up to 255 colours) |
| **Remove last** | Takes the last colour out |
| **Create palette** | Only while the picture names no palette: makes one and names it |
Only the **last** colour can be removed. Pixels store their colour by index, so taking a colour out of
the middle would shift every pixel above it, in every picture painted with the palette. A pixel whose
index runs past the end of the palette is transparent.
## Tools
The **TOOLS** block holds seven tools. A stroke, a fill or a shape is **one undo step**.
| Tool | What a press does |
|---|---|
| **Pencil** | Paints the picked colour; a drag paints a gapless line under the pointer |
| **Eraser** | The same, with transparent |
| **Fill** | Fills the area of the clicked colour, joined along edges (not corners). With **Contiguous** off, every pixel of that colour on the layer |
| **Picker** | Picks the colour under the pointer: the top visible layer that paints the pixel, else transparent |
| **Line** | A straight line from the press to the release |
| **Rectangle** | The border of the rectangle between press and release, or all of it with **Filled shapes** |
| **Ellipse** | The ellipse that fits that rectangle, as a one pixel outline or filled |
**Alt+click** picks a colour with any tool. **Brush** sets the pencil, the eraser and the outlines
of line, rectangle and ellipse from 1 to 8 pixels; an even brush reaches one pixel further right and
down than left and up.
There are no keyboard shortcuts for the tools yet. Selecting, copying and mirroring are
[#198](https://git.nockal.com/meleki/game-maker/issues/198), more tools (symmetry, lasso, dithering,
replacing a colour) are [#200](https://git.nockal.com/meleki/game-maker/issues/200).
## Layers
The **LAYERS** list shows the top layer first. A click on a row picks the layer the tools paint on.
- **Add** puts a new empty layer above the picked one, **Delete** takes the picked one out. The last
layer stays.
- **Up** and **Down** move it in the drawing order.
- **Hide** leaves it out of the picture, **Show** puts it back. A hidden layer cannot be painted on.
- **Name** and **Opacity** (0 to 1) edit the picked layer.
- **Merge down** lays the layer onto the one below it, on every frame, and takes it out. Every pixel
it paints covers the one below. Its opacity is **not** kept: an indexed picture has no colour
between two palette entries, so the merged layer keeps the opacity of the lower one.
## Frames
Every frame has the same layers. The **FRAMES** list shows them in order; a click picks the frame
the canvas shows and paints on.
- **Add** puts an empty frame after the picked one, **Duplicate** a copy of it. The last frame stays.
- **Earlier** and **Later** move the picked frame.
- **Onion skin** (in the **VIEW** block) shows the frames before and after the picked one faintly
under it.
- **PREVIEW** plays the frames in a loop. **Frames per second** belongs to the preview only; how long
a frame lasts in the game is up to the [Animation](Animations) that keys it.
**How frames are worn.** All frames of an asset are drawn side by side into one picture, and each
frame is a cut of it, like a frame of a sliced texture. Once there is more than one frame, a *Texture*
row offers the asset and each frame by name (*Walk / Frame 2*). **Create sprite from picture** in the
assets panel's menu makes a sprite wearing the first frame, and **Create animation from frames**
makes a sprite and an animation that keys every frame in order, one step each.
## The view
The canvas zooms with the wheel in whole steps (1x, 2x, 3x, 4x, 6x, 8x, 12x, 16x, 24x, 32x), so every
pixel is a square of the same size on screen. From 8x on a grid runs between the pixels. Shift+drag or
the middle mouse button pans, **Fit** picks the largest step that fits. **Tile preview** in the
**VIEW** block repeats the picture around the canvas, three by three, for tiles that have to join.
## How the pixels are stored
The project file holds names, not pixels. The pixels of one layer on one frame, a **cel**, are a file
of their own on the server, `data/pixels/<sha256>.px`, named by the hash of its bytes like an
uploaded image. An empty cel is no file at all. A stroke names its cel at once, in the browser, and
the cels the server does not have yet are uploaded **before** the project that names them is saved.
Undo swaps names, so it never waits for the server. The format is in the [API](API).
## Known limits
- No import of an existing texture yet ([#197](https://git.nockal.com/meleki/game-maker/issues/197))
and no export as PNG ([#199](https://git.nockal.com/meleki/game-maker/issues/199)).
- The palette is edited in the Pixel art tab; selecting the palette asset itself shows no colours.
- A cel file stays on the server when nothing names it any more, as an uploaded image does.
+10 -8
@@ -196,21 +196,23 @@ counting placed instances would report "unused" for a picture a dozen sprites ar
## A picture can be drawn instead of uploaded
Not every picture is a file: an asset may draw its own, and the first one that does is
[Vector Art](Vector-Art) (#129). Such an asset is worn exactly like a texture — it stands in the
Not every picture is a file: an asset may draw its own. Two do, [Vector Art](Vector-Art) (#129)
and [Pixel Art](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. It has **no frames** — frames are rectangles cut out of a
sheet, and a drawn picture has none to cut from. And it is **not stored**: nothing of it lies in
`data/textures`, the project file holds the drawing, and the picture is drawn again after a
reload. 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.
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 drawn picture is rasterised at **twice** the resolution of its canvas, so zooming into it does
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.
+3 -2
@@ -50,8 +50,9 @@ asks for; a picture that does not fit is kept and named, never silently dropped.
- **Pictures** — one block per animation step, in the order they run. A picture is either a
**texture** (a file that holds nothing but the one block), a **frame** of a texture (one block
cut out of a sheet that holds many), or a [vector drawing](Vector-Art), which the strip offers
like any other picture — it has no frames, so it is always the whole canvas. Cutting frames is
cut out of a sheet that holds many), a [vector drawing](Vector-Art), which the strip offers
like any other picture — it has no frames, so it is always the whole canvas —, or
[pixel art](Pixel-Art), whole or one of its frames. Cutting frames is
the [Textures](Textures) tab, by hand or with **Slice…** where the sheet is a regular grid.
- **Kind** — *None (1 × 1)*, *Floor (2 × 3)* or *Wall (2 × 2)*. Chosen, not guessed: a sheet whose rows alternate
between the two shapes (`Outside_A4` in RPG Maker) would be read wrong either way, and the choice
+1
@@ -9,6 +9,7 @@
- [Physics](Physics)
- [Textures](Textures)
- [Vector art](Vector-Art)
- [Pixel art](Pixel-Art)
- [Fonts](Fonts)
- [Sounds](Sounds)
- [Animations](Animations)