.mstack file format
The native, fully-editable project format. A .mstack file is a ZIP archive containing a JSON manifest and one PNG per layer buffer. You can open it in any other instance of this editor and pick up exactly where you left off.
Beta notice: the format is at schema v5 as of v0.15.0. While in beta, schema versions can break compatibility — once a
.mstackfile is on disk it's pinned to that version. Treat exports as work-in-progress.
Container
A standard ZIP file with the following layout:
project.mstack (ZIP)
├── manifest.json # canonical project state (zod-validated)
├── thumbnail.png # 256 px max preview for project listings
└── layers/
├── <bufferKey>.png # one PNG per unique buffer key
└── ...- MIME type:
application/x-mstack - Extension:
.mstack - ZIP library:
fflate(compression level 6)
manifest.json
A JSON document validated by the zod schema. Top-level fields:
| Field | Type | Description |
|---|---|---|
schemaVersion | int | Currently 5. Refusal to load if not exactly this. |
app | { name, version } | Editor version that wrote the file |
project | { id, name, canvasWidth, canvasHeight, createdAt, updatedAt } | Project metadata |
frames | Record<FrameId, Frame> | All frames keyed by id |
frameOrder | FrameId[] | Display order |
activeFrameId | FrameId | Last-viewed frame |
activeLayerId | LayerId | Last-active layer in that frame |
sprites | Record<SpriteId, Sprite> | Named subsets of frames |
spriteOrder | SpriteId[] | Display order |
activeSpriteId | SpriteId | null | Filter state |
animations | Record<AnimationId, Animation> | Playback sequences |
animationOrder | AnimationId[] | Display order |
activeAnimationId | AnimationId | null | Active animation |
primaryColor | hex string | FG colour |
secondaryColor | hex string | BG colour |
palette | string[] | Hex palette entries |
tilePreview | bool | Tile preview toggle |
pixelPerfect | bool | Pixel-perfect toggle |
symmetry | object | Symmetry state |
ramps | array | Color ramps |
activeRampId | string | null | Active ramp |
colorMode | 'rgb' | 'indexed' | Indexed mode |
cycles | array | Color cycles |
customBrushes | array | Custom brushes (mask as base64) |
frameTags | array | Frame tags |
onionTintBefore | hex | null | Onion skin before tint |
onionTintAfter | hex | null | After tint |
Frame shape
{
id: string,
name: string,
width: number,
height: number,
layers: Layer[], // ordered bottom-up
durationMs: number, // default playback duration
groups: LayerGroup[] // layer groups for this frame
}Layer shape
{
id: string,
name: string,
visible: boolean,
locked: boolean,
opacity: number, // 0-1
blendMode: 'normal' | 'multiply' | 'screen',
linkKey?: string, // when set, this layer shares its buffer
parentGroupId?: string // when set, this layer is in a group
}LayerGroup shape
{
id: string,
name: string,
visible: boolean,
locked: boolean,
collapsed: boolean,
opacity: number, // 0-1
blendMode: 'normal' | 'multiply' | 'screen'
}FrameTag shape
{
id: string,
name: string,
fromIndex: number, // 0-based, inclusive
toIndex: number, // 0-based, inclusive
color: string, // hex
direction: 'forward' | 'reverse' | 'pingpong'
}ColorCycle shape
{
id: string,
name: string,
slotIndices: number[], // palette indices that rotate
stepMs: number,
enabled: boolean
}CustomBrush shape
{
id: string,
name: string,
width: number,
height: number,
maskB64: string // base64 of width*height bytes; 1 = paint, 0 = skip
}Layer PNGs
The layers/ directory contains one PNG per unique buffer key:
- For an unlinked layer, the buffer key equals the layer id.
- For a linked layer, the buffer key is the layer's
linkKey— a single PNG is shared by every layer with that link key.
Each PNG is the same dimensions as the frame's width × height and contains RGBA pixels.
This dedup keeps file sizes reasonable for projects with linked backgrounds across many frames.
thumbnail.png
A preview rendered at the moment of save, capped at 256 px on the longest dimension and using nearest-neighbour scaling. Used in project listings (e.g. the File → Open project dialog) so you can identify projects without opening them.
The thumbnail is generated from the active frame's composed layers at save time.
Versioning policy
While the editor is in beta:
- Each schema bump (
v5 → v6 → …) is potentially breaking. - The reader rejects any file with
schemaVersion≠ the editor's current value. There's intentionally no migration ladder. - After 1.0, the policy will switch to: forward-compatible reads (always work) + explicit migrations on bumps.
For now, treat your .mstack files as transient. If you need long-term archival, also export to PNG / GIF / sprite sheet so the rendered output isn't lost when schemas change.
Related
- Export formats
- Source code —
schema.ts,reader.ts,writer.ts