Skip to content

.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 .mstack file 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:

FieldTypeDescription
schemaVersionintCurrently 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
framesRecord<FrameId, Frame>All frames keyed by id
frameOrderFrameId[]Display order
activeFrameIdFrameIdLast-viewed frame
activeLayerIdLayerIdLast-active layer in that frame
spritesRecord<SpriteId, Sprite>Named subsets of frames
spriteOrderSpriteId[]Display order
activeSpriteIdSpriteId | nullFilter state
animationsRecord<AnimationId, Animation>Playback sequences
animationOrderAnimationId[]Display order
activeAnimationIdAnimationId | nullActive animation
primaryColorhex stringFG colour
secondaryColorhex stringBG colour
palettestring[]Hex palette entries
tilePreviewboolTile preview toggle
pixelPerfectboolPixel-perfect toggle
symmetryobjectSymmetry state
rampsarrayColor ramps
activeRampIdstring | nullActive ramp
colorMode'rgb' | 'indexed'Indexed mode
cyclesarrayColor cycles
customBrushesarrayCustom brushes (mask as base64)
frameTagsarrayFrame tags
onionTintBeforehex | nullOnion skin before tint
onionTintAfterhex | nullAfter tint

Frame shape

ts
{
  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

ts
{
  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

ts
{
  id: string,
  name: string,
  visible: boolean,
  locked: boolean,
  collapsed: boolean,
  opacity: number,        // 0-1
  blendMode: 'normal' | 'multiply' | 'screen'
}

FrameTag shape

ts
{
  id: string,
  name: string,
  fromIndex: number,                                // 0-based, inclusive
  toIndex: number,                                  // 0-based, inclusive
  color: string,                                    // hex
  direction: 'forward' | 'reverse' | 'pingpong'
}

ColorCycle shape

ts
{
  id: string,
  name: string,
  slotIndices: number[],   // palette indices that rotate
  stepMs: number,
  enabled: boolean
}

CustomBrush shape

ts
{
  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.


Motestack is a personal hobby project. The editor and these docs ship under no warranty — back up your `.mstack` files.