moonbit-aseflow

A MoonBit pipeline for Aseprite JSON sprite animation assets.

aseprite
sprite
animation
atlas
gamedev
moon add lllg123/moonbit-aseflow@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
7 days ago
Downloads
2
README

#moonbit-aseflow

moonbit-aseflow is a MoonBit library and small CLI preview for turning Aseprite JSON sprite-sheet exports into engine-facing asset data.

The project is aimed at pixel-art game workflows where artists work in Aseprite, while MoonBit code needs stable data for animation clips, texture atlas placement, nine-patch UI slices, collision boxes, and state-machine exports.

#Scope

  • Parse Aseprite array-style JSON exports.
  • Parse Aseprite object-style JSON exports with deterministic filename ordering.
  • Normalize frame rectangles, source sizes, durations, frame tags, and slices.
  • Build animation clips from frameTags.
  • Produce a deterministic row-based atlas layout plan.
  • Extract nine-patch metadata from Aseprite slices.
  • Attach named collision boxes to frames.
  • Convert convention-based collision and hitbox slices into frame boxes.
  • Export a combined JSON bundle for game runtimes and tools.
  • Report diagnostics for invalid geometry, duplicate names, tag ranges, and slice keyframes.
  • Produce reproducible benchmark reports from the checked-in fixtures.

The current package intentionally keeps image encoding out of the core. It produces a layout plan that a later PNG backend, build script, or game engine importer can consume.

#Quick Start

moon check moon test moon run cmd/main

The CLI prints a compact export bundle from a built-in sample. Library users can call parse_aseprite_json and export_bundle_json with JSON text from an Aseprite export.

The repository includes public-format fixtures under examples/benchmarks. Use benchmark_report when a build needs a stable summary for regression checks.

///|
test {
let json =
#|{
#| "frames": [{
#| "filename": "hero_idle_0",
#| "frame": {"x": 0, "y": 0, "w": 16, "h": 24},
#| "rotated": false,
#| "trimmed": false,
#| "spriteSourceSize": {"x": 0, "y": 0, "w": 16, "h": 24},
#| "sourceSize": {"w": 16, "h": 24},
#| "duration": 100
#| }],
#| "meta": {
#| "image": "hero.png",
#| "size": {"w": 16, "h": 24},
#| "frameTags": [{"name": "idle", "from": 0, "to": 0, "direction": "forward"}],
#| "slices": []
#| }
#|}
let sheet = parse_aseprite_json(json)
let machine = build_machine(sheet)
inspect(machine.initial, content="idle")
}

#Validation

The repository is checked with the same commands used by CI:

moon check --target all --deny-warn moon test --target wasm moon test --target wasm-gc moon test --target js moon fmt --check moon info moon run cmd/main

moon check --deny-warn is the warning-as-error gate for the current toolchain. In moon 0.1.20260713, fmt and info do not accept --deny-warn, so CI uses their supported strict forms and verifies that generated files leave no diff. Native tests also pass the build plan when a C compiler is installed. On Windows machines without cl, cc, gcc, or clang in PATH, moon test --target native stops before compiling user code.

#Repository Notes

  • License: Apache-2.0.
  • Source and prior-art notes: docs/SOURCES.md.
  • Benchmark data and provenance: docs/BENCHMARKS.md.
  • Hackathon self-check: docs/HACKATHON_CHECKLIST.md.
  • The MoonBit module lives at repository root so automated checks can run without extra -C flags.

#Roadmap

  • Add file/stdin CLI when the stable MoonBit I/O layer is selected for the target backend.
  • Add PNG atlas emission as an optional backend.
  • Add engine presets for Godot, Phaser, and custom MoonBit runtimes.

#
AnimationError

pub suberror AnimationError {
TagOutOfRange(String)
EmptyTagName
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
AtlasError

pub suberror AtlasError {
InvalidMaxWidth(Int)
InvalidPadding(Int)
FrameTooWide(String, Int, Int)
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
AnimationClip

pub struct AnimationClip {
name : String
frames : Array[String]
durations : Array[Int]
direction : Direction
loop_mode : LoopMode
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
AnimationClip::total_duration

fn AnimationClip::total_duration(self : AnimationClip) -> Int

#
AnimationMachine

pub struct AnimationMachine {
initial : String
clips : Array[AnimationClip]
transitions : Array[StateTransition]
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
AnimationMachine::clip_named

fn AnimationMachine::clip_named(self : AnimationMachine, name : String) -> AnimationClip?

#
AnimationMachine::to_json_string

fn AnimationMachine::to_json_string(self : AnimationMachine, indent? : Int) -> String

#
AnimationMachine::transition_target

fn AnimationMachine::transition_target(self : AnimationMachine, from~ : String, event~ : String) -> String?

#
AnimationMachine::with_transition

fn AnimationMachine::with_transition(self : AnimationMachine, from~ : String, event~ : String, to~ : String) -> AnimationMachine

#
AtlasOptions

pub struct AtlasOptions {
max_width : Int
padding : Int
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
AtlasOptions::default

fn AtlasOptions::default() -> AtlasOptions

#
AtlasPlan

pub struct AtlasPlan {
image : String
size : Size
frames : Array[PackedFrame]
occupancy : Double
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
AtlasPlan::packed_area

fn AtlasPlan::packed_area(self : AtlasPlan) -> Int

#
AtlasPlan::to_json_string

fn AtlasPlan::to_json_string(self : AtlasPlan, indent? : Int) -> String

#
BenchmarkReport

pub struct BenchmarkReport {
name : String
frames : Int
tags : Int
slices : Int
total_duration : Int
atlas_size : Size
packed_area : Int
occupancy : Double
errors : Int
warnings : Int
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
BenchmarkReport::to_json_string

fn BenchmarkReport::to_json_string(self : BenchmarkReport, indent? : Int) -> String

#
CollisionBox

pub struct CollisionBox {
name : String
rect : Rect
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
Diagnostic

pub struct Diagnostic {
severity : Severity
code : String
message : String
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
DiagnosticSummary

pub struct DiagnosticSummary {
errors : Int
warnings : Int
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
Direction

pub enum Direction {
Forward
Reverse
PingPong
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
Direction::from_string

fn Direction::from_string(text : String) -> Direction

#
ExportBundle

pub struct ExportBundle {
sheet : SpriteSheet
atlas : AtlasPlan
machine : AnimationMachine
nine_patches : Array[NinePatch]
diagnostics : Array[Diagnostic]
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
FrameTag

pub struct FrameTag {
name : String
from : Int
to : Int
direction : Direction
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
NinePatch

pub struct NinePatch {
name : String
outer : Rect
center : Rect
left : Int
right : Int
top : Int
bottom : Int
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
PackedFrame

pub struct PackedFrame {
filename : String
source : Rect
packed : Rect
duration : Int
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
RawObjectAsepriteSheet

#
Rect

pub struct Rect {
x : Int
y : Int
w : Int
h : Int
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
Rect::area

fn Rect::area(self : Rect) -> Int

#
Rect::bottom

fn Rect::bottom(self : Rect) -> Int

#
Rect::contains_rect

fn Rect::contains_rect(self : Rect, other : Rect) -> Bool

#
Rect::is_empty

fn Rect::is_empty(self : Rect) -> Bool

#
Rect::right

fn Rect::right(self : Rect) -> Int

#
Severity

#
Size

#
Size::area

fn Size::area(self : Size) -> Int

#
Slice

pub struct Slice {
name : String
color : String?
bounds : Rect
center : Rect?
pivot : Point?
keys : Array[SliceKey]
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
Slice::key_at

fn Slice::key_at(self : Slice, frame : Int) -> SliceKey?

#
Slice::to_nine_patch

fn Slice::to_nine_patch(self : Slice) -> NinePatch?

#
Slice::to_nine_patch_at

fn Slice::to_nine_patch_at(self : Slice, frame : Int) -> NinePatch?

#
SliceKey

pub struct SliceKey {
frame : Int
bounds : Rect
center : Rect?
pivot : Point?
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
SourceFrame

pub struct SourceFrame {
filename : String
frame : Rect
rotated : Bool
trimmed : Bool
sprite_source_size : Rect
source_size : Size
duration : Int
boxes : Array[CollisionBox]
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
SourceFrame::with_box

fn SourceFrame::with_box(self : SourceFrame, name~ : String, rect~ : Rect) -> SourceFrame

#
SpriteSheet

pub struct SpriteSheet {
image : String
size : Size
frames : Array[SourceFrame]
tags : Array[FrameTag]
slices : Array[Slice]
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
SpriteSheet::find_frame

fn SpriteSheet::find_frame(self : SpriteSheet, filename : String) -> SourceFrame?

#
SpriteSheet::frame_at

fn SpriteSheet::frame_at(self : SpriteSheet, index : Int) -> SourceFrame?

#
SpriteSheet::with_box

fn SpriteSheet::with_box(self : SpriteSheet, filename~ : String, name~ : String, rect~ : Rect) -> SpriteSheet

#
StateTransition

pub struct StateTransition {
from : String
event : String
to : String
} derive(Eq, ToJson,
Debug
,
FromJson
)

#
attach_collision_slices

fn attach_collision_slices(sheet : SpriteSheet) -> SpriteSheet

#
benchmark_report

fn benchmark_report(name : String, text : String, atlas? : AtlasOptions) -> BenchmarkReport raise

#
build_export_bundle

fn build_export_bundle(sheet : SpriteSheet, atlas? : AtlasOptions) -> ExportBundle

#
build_machine

fn build_machine(sheet : SpriteSheet, initial? : String) -> AnimationMachine

#
build_machine_checked

fn build_machine_checked(sheet : SpriteSheet, initial? : String) -> AnimationMachine raise

#
collect_nine_patches

fn collect_nine_patches(sheet : SpriteSheet) -> Array[NinePatch]

#
collect_nine_patches_at

fn collect_nine_patches_at(sheet : SpriteSheet, frame : Int) -> Array[NinePatch]

#
convert_aseprite_json

fn convert_aseprite_json(text : String, indent? : Int) -> String raise

#
diagnostics_to_json

fn diagnostics_to_json(diagnostics : Array[Diagnostic], indent? : Int) -> String

#
export_bundle_json

fn export_bundle_json(sheet : SpriteSheet, atlas? : AtlasOptions, indent? : Int) -> String

#
has_errors

fn has_errors(diagnostics : ArrayView[Diagnostic]) -> Bool

#
pack_rows

fn pack_rows(sheet : SpriteSheet, options? : AtlasOptions) -> AtlasPlan

#
pack_rows_checked

fn pack_rows_checked(sheet : SpriteSheet, options? : AtlasOptions) -> AtlasPlan raise

#
parse_aseprite_json

fn parse_aseprite_json(text : String) -> SpriteSheet raise

#
parse_sheet

fn parse_sheet(text : String) -> SpriteSheet raise

#
sheet_to_json

fn sheet_to_json(sheet : SpriteSheet, indent? : Int) -> String

#
summarize_diagnostics

fn summarize_diagnostics(diagnostics : ArrayView[Diagnostic]) -> DiagnosticSummary

#
total_duration

fn total_duration(sheet : SpriteSheet) -> Int

#
validate_sheet

fn validate_sheet(sheet : SpriteSheet) -> Array[Diagnostic]

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io