diff --git a/Cargo.lock b/Cargo.lock
index 0ab7069f7382cc28822c67cbe041d7afa186b71f..7b4f82e02e796456e4c447564f0670e508011aac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,12 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "aligned-vec"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
+
 [[package]]
 name = "anyhow"
 version = "1.0.86"
@@ -23,12 +29,67 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
 [[package]]
 name = "autocfg"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
+[[package]]
+name = "av1-grain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "bit_field"
 version = "0.10.2"
@@ -41,6 +102,24 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "bitstream-io"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8"
+
+[[package]]
+name = "built"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17"
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
 [[package]]
 name = "by_address"
 version = "1.2.1"
@@ -59,6 +138,33 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "cc"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2755ff20a1d93490d26ba33a6f092a38a508398a5320df5d4b3014fcccce9410"
+dependencies = [
+ "jobserver",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -71,12 +177,6 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
-[[package]]
-name = "convert_case"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
-
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -117,19 +217,6 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
-[[package]]
-name = "derive_more"
-version = "0.99.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
-dependencies = [
- "convert_case",
- "proc-macro2",
- "quote",
- "rustc_version",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "either"
 version = "1.12.0"
@@ -238,24 +325,51 @@ version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
 [[package]]
 name = "image"
-version = "0.24.9"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
+checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
 dependencies = [
  "bytemuck",
  "byteorder",
  "color_quant",
  "exr",
  "gif",
- "jpeg-decoder",
+ "image-webp",
  "num-traits",
  "png",
  "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
  "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d"
+dependencies = [
+ "byteorder-lite",
+ "thiserror",
 ]
 
+[[package]]
+name = "imgref"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
+
 [[package]]
 name = "indexmap"
 version = "2.2.6"
@@ -266,20 +380,46 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
+[[package]]
+name = "jobserver"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "jpeg-decoder"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
-dependencies = [
- "rayon",
-]
 
 [[package]]
 name = "kmeans_colors"
@@ -305,6 +445,17 @@ version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.12"
@@ -317,9 +468,28 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.21"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
 
 [[package]]
 name = "memchr"
@@ -327,6 +497,12 @@ version = "2.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.7.3"
@@ -337,6 +513,69 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -346,6 +585,12 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
 [[package]]
 name = "palette"
 version = "0.7.6"
@@ -366,9 +611,21 @@ dependencies = [
  "by_address",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn",
 ]
 
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
 [[package]]
 name = "png"
 version = "0.17.13"
@@ -397,6 +654,25 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "profiling"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
+dependencies = [
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "qoi"
 version = "0.4.1"
@@ -406,6 +682,12 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
 [[package]]
 name = "quote"
 version = "1.0.36"
@@ -445,6 +727,56 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "rav1e"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
+dependencies = [
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "once_cell",
+ "paste",
+ "profiling",
+ "rand",
+ "rand_chacha",
+ "simd_helpers",
+ "system-deps",
+ "thiserror",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67376f469e7e7840d0040bbf4b9b3334005bb167f814621326e4c7ab8cd6e944"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
 [[package]]
 name = "rayon"
 version = "1.10.0"
@@ -466,12 +798,12 @@ dependencies = [
 ]
 
 [[package]]
-name = "rustc_version"
-version = "0.4.0"
+name = "rgb"
+version = "0.8.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741"
 dependencies = [
- "semver",
+ "bytemuck",
 ]
 
 [[package]]
@@ -495,12 +827,6 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
-[[package]]
-name = "semver"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
-
 [[package]]
 name = "serde"
 version = "1.0.203"
@@ -518,7 +844,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn",
 ]
 
 [[package]]
@@ -549,6 +875,15 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 
+[[package]]
+name = "simd_helpers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
+dependencies = [
+ "quote",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.13.2"
@@ -569,7 +904,7 @@ name = "sprite_processor"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "derive_more",
+ "bincode",
  "fxhash",
  "image",
  "kmeans_colors",
@@ -578,15 +913,14 @@ dependencies = [
  "rayon",
  "serde",
  "serde_yaml",
- "toml",
  "walkdir",
 ]
 
 [[package]]
 name = "syn"
-version = "1.0.109"
+version = "2.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -594,14 +928,42 @@ dependencies = [
 ]
 
 [[package]]
-name = "syn"
-version = "2.0.66"
+name = "system-deps"
+version = "6.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
+
+[[package]]
+name = "thiserror"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
 dependencies = [
  "proc-macro2",
  "quote",
- "unicode-ident",
+ "syn",
 ]
 
 [[package]]
@@ -661,6 +1023,23 @@ version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
 
+[[package]]
+name = "v_frame"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
 [[package]]
 name = "walkdir"
 version = "2.5.0"
@@ -677,6 +1056,60 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
 [[package]]
 name = "weezl"
 version = "0.1.8"
@@ -774,6 +1207,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "zune-core"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
+
 [[package]]
 name = "zune-inflate"
 version = "0.2.54"
@@ -782,3 +1221,12 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
 dependencies = [
  "simd-adler32",
 ]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
+dependencies = [
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 02dffcd30d39e6cff42beb66a4e6c874c5f340d6..e8281bea27636516a5c3a6457b0e513b19a75890 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,17 +7,16 @@ edition = "2021"
 
 [dependencies]
 anyhow = "1.0.86"
-image       = "0.24.9"
-log         = "0.4.20"
-serde       = { version = "1.0.197", features = ["derive"]}
-serde_yaml = "0.9.17"
-toml        = "0.8.10"
-rayon       = "1.5.1"
-walkdir     = "2.3.2"
+image       = "0.25.1"
+log         = "0.4.22"
+serde       = { version = "1.0.203", features = ["derive"]}
+serde_yaml = "0.9.33"
+rayon       = "1.10.0"
+walkdir     = "2.5.0"
 kmeans_colors = { version = "0.6.0", default-features = false, features = ["palette_color"] }
 fxhash = { version = "0.2.1" }
 palette = { version = "0.7.6", default-features = false, features = ["std"] }
-derive_more = "0.99.17"
+bincode = "1.3.3"
 
 [profile.dev.package."*"]
 opt-level = 3
diff --git a/source_assets/fallback/sprites/background/background.gif b/source_assets/fallback/animations/background.gif
similarity index 100%
rename from source_assets/fallback/sprites/background/background.gif
rename to source_assets/fallback/animations/background.gif
diff --git a/source_assets/fallback/sprites/diver/diver.gif b/source_assets/fallback/animations/diver.gif
similarity index 100%
rename from source_assets/fallback/sprites/diver/diver.gif
rename to source_assets/fallback/animations/diver.gif
diff --git a/source_assets/fallback/sprites/ink/ink.gif b/source_assets/fallback/animations/ink.gif
similarity index 100%
rename from source_assets/fallback/sprites/ink/ink.gif
rename to source_assets/fallback/animations/ink.gif
diff --git a/source_assets/fallback/sprites/kraken/kraken.gif b/source_assets/fallback/animations/kraken.gif
similarity index 100%
rename from source_assets/fallback/sprites/kraken/kraken.gif
rename to source_assets/fallback/animations/kraken.gif
diff --git a/source_assets/fallback/sprites/shark/shark.gif b/source_assets/fallback/animations/shark.gif
similarity index 100%
rename from source_assets/fallback/sprites/shark/shark.gif
rename to source_assets/fallback/animations/shark.gif
diff --git a/source_assets/fallback/sprites/speedboat/speedboat.gif b/source_assets/fallback/animations/speedboat.gif
similarity index 100%
rename from source_assets/fallback/sprites/speedboat/speedboat.gif
rename to source_assets/fallback/animations/speedboat.gif
diff --git a/source_assets/fallback/sprites/sub/sub_side.gif b/source_assets/fallback/animations/sub_side.gif
similarity index 100%
rename from source_assets/fallback/sprites/sub/sub_side.gif
rename to source_assets/fallback/animations/sub_side.gif
diff --git a/source_assets/fallback/sprites/sub/sub_turn.gif b/source_assets/fallback/animations/sub_turn.gif
similarity index 100%
rename from source_assets/fallback/sprites/sub/sub_turn.gif
rename to source_assets/fallback/animations/sub_turn.gif
diff --git a/source_assets/fallback/sprites/torpedo/torpedo.gif b/source_assets/fallback/animations/torpedo.gif
similarity index 100%
rename from source_assets/fallback/sprites/torpedo/torpedo.gif
rename to source_assets/fallback/animations/torpedo.gif
diff --git a/source_assets/fallback/sprites/explosions/torpedo_explosion.gif b/source_assets/fallback/animations/torpedo_explosion.gif
similarity index 100%
rename from source_assets/fallback/sprites/explosions/torpedo_explosion.gif
rename to source_assets/fallback/animations/torpedo_explosion.gif
diff --git a/source_assets/fallback/config.yml b/source_assets/fallback/config.yml
index 10f3ee7fdf314644ab5ab4805ac080e67a5ebdf7..24af84d6117402ff2f3368cdc4e347abee818276 100644
--- a/source_assets/fallback/config.yml
+++ b/source_assets/fallback/config.yml
@@ -1,80 +1,116 @@
-theme_name: "fallback"
-palette_sets:
-  - { name: "p1", animation: "sub_side", palettes: ["p1", "p1", "p1", "p1"] }
-  - { name: "p2", animation: "sub_side", palettes: ["p2", "p2", "p2", "p2"] }
-  - { name: "low_air", animation: "sub_side", palettes: ["low_air", "", "", ""] }
-  - { name: "critical_air", animation: "sub_side", palettes: ["low_air", "", "critical_air", ""] }
-animations:
-  - { animation: "background", offset: [0, 0], max_colors: 255 }
-  - { animation: "diver", offset: [0, 0], max_colors: 12 }
-  - { animation: "kraken", offset: [6, 2], max_colors: 12 }
-  - { animation: "shark", offset: [14, 3], max_colors: 12 }
-  - { animation: "speedboat", offset: [0, 0], max_colors: 12 }
-  - { animation: "sub_side", offset: [9, 0], max_colors: 12 }
-  - { animation: "torpedo", offset: [0, 0], max_colors: 12 }
-  - { animation: "ink", offset: [0, 0], max_colors: 12 }
-  - { animation: "explosions", offset: [0, 0], max_colors: 12 }
-entities:
-  - {
-    name: "background",
-    mappings: [
-      { action: "background", sprite: "background", animation: "background" }]
-  }
-  - {
-    name: "player1",
-    mappings: [
-      { action: "idle", sprite: "sub", animation: "sub_side" },
-      { action: "move", sprite: "sub", animation: "sub_side"  },
-      { action: "fire", sprite: "sub", animation: "sub_side" },
-      { action: "death", sprite: "sub", animation: "sub_side" } ]
-  }
-  - {
-    name: "player2",
-    mappings: [
-      { action: "idle", sprite: "sub", animation: "sub_side" },
-      { action: "move", sprite: "sub", animation: "sub_side" },
-      { action: "fire", sprite: "sub", animation: "sub_side" },
-      { action: "death", sprite: "sub", animation: "sub_side" }]
-  }
-  - {
-    name: "patrol_sub",
-    mappings: [
-      { action: "idle", sprite: "speedboat", animation: "speedboat" },
-      { action: "move", sprite: "speedboat", animation: "speedboat" },
-      { action: "fire", sprite: "speedboat", animation: "speedboat" },
-      { action: "death", sprite: "speedboat", animation: "speedboat" }]
-  }
-  - {
-    name: "shark",
-    mappings: [
-      { action: "idle", sprite: "shark", animation: "shark"},
-      { action: "move", sprite: "shark", animation: "shark"},
-      { action: "fire", sprite: "shark", animation: "shark" },
-      { action: "death", sprite: "shark", animation: "shark"}]
-  }
-  - {
-    name: "bullet",
-    mappings: [
-      { action: "idle", sprite: "torpedo", animation: "torpedo", palette: "torpedo-default" },
-      { action: "collision", sprite: "explosions", animation: "torpedo_explosion", palette: "explosions-default" }
-    ]
-  }
-  - {
-    name: "ink",
-    mappings: [
-      { action: "idle", sprite: "ink", animation: "ink", palette: "ink-default" },
-      { action: "collision", sprite: "explosions", animation: "torpedo_explosion", palette: "explosions-default" }
-    ]
-  }
-  - {
-    name: "diver",
-    mappings: [{ action: "idle", sprite: "diver", animation: "diver", palette: "diver-default" }]
-  }
-  - {
-    name: "sub",
-    mappings: [
-      { action: "idle", sprite: "kraken", animation: "kraken", palette: "kraken-default" },
-      { action: "move", sprite: "kraken", animation: "kraken", palette: "kraken-default" },
-      { action: "fire", sprite: "kraken", animation: "kraken", palette: "kraken-default" },
-      { action: "death", sprite: "kraken", animation: "kraken", palette: "kraken-default" }]
-  }
+{
+  name: "fallback",
+  required_mappings: [
+    { game_entity: "player1", animation_states: ["idle", "move"] },
+    { game_entity: "player2", animation_states: ["idle", "move"] },
+    { game_entity: "shark", animation_states: ["move"] },
+    { game_entity: "sub", animation_states: ["move"] },
+    { game_entity: "ink", animation_states: ["move"] },
+    { game_entity: "patrol_sub", animation_states: ["move"] },
+    { game_entity: "bullet", animation_states: ["move"] },
+    { game_entity: "diver", animation_states: ["move"] },
+    { game_entity: "background", animation_states: ["background"] },
+    { game_entity: "explosion", animation_states: ["explosion"] }
+  ],
+  animations: [
+    {
+      source_animation: "sub_side",
+      offset: [ 9, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "player1", "player2" ],
+            animation_states: [ "idle", "move" ],
+          },
+    },
+    {
+      source_animation: "shark",
+      offset: [ 14, 3 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "shark" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "kraken",
+      offset: [ 6, 2 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "sub" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "ink",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "ink" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "speedboat",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "patrol_sub" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "torpedo",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "bullet" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "diver",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "diver" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "background",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 255,
+      game_entity_map:
+        {
+          game_entities: [ "background" ],
+          animation_states: [ "background" ],
+        },
+    },
+    {
+      source_animation: "torpedo_explosion",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 15,
+      game_entity_map:
+          {
+            game_entities: [ "explosion" ],
+            animation_states: [ "explosion" ],
+          },
+    },
+  ]
+}
diff --git a/source_assets/fallback/sprites/shark/palette_set_0.palette.png b/source_assets/fallback/sprites/shark/palette_set_0.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/shark/palette_set_0.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/shark/palette_set_1.palette.png b/source_assets/fallback/sprites/shark/palette_set_1.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/shark/palette_set_1.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/shark/palette_set_2.palette.png b/source_assets/fallback/sprites/shark/palette_set_2.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/shark/palette_set_2.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/shark/palette_set_3.palette.png b/source_assets/fallback/sprites/shark/palette_set_3.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/shark/palette_set_3.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/sub/critical_air.palette.png b/source_assets/fallback/sprites/sub/critical_air.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/sub/critical_air.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/sub/low_air.palette.png b/source_assets/fallback/sprites/sub/low_air.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/sub/low_air.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/sub/p1.palette.png b/source_assets/fallback/sprites/sub/p1.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/sub/p1.palette.png and /dev/null differ
diff --git a/source_assets/fallback/sprites/sub/p2.palette.png b/source_assets/fallback/sprites/sub/p2.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/fallback/sprites/sub/p2.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/background/background.gif b/source_assets/joe/animations/background.gif
similarity index 100%
rename from source_assets/joe/sprites/background/background.gif
rename to source_assets/joe/animations/background.gif
diff --git a/source_assets/joe/sprites/diver/diver.gif b/source_assets/joe/animations/diver.gif
similarity index 100%
rename from source_assets/joe/sprites/diver/diver.gif
rename to source_assets/joe/animations/diver.gif
diff --git a/source_assets/joe/sprites/torpedo/torpedo.gif b/source_assets/joe/animations/ink.gif
similarity index 100%
rename from source_assets/joe/sprites/torpedo/torpedo.gif
rename to source_assets/joe/animations/ink.gif
diff --git a/source_assets/joe/sprites/kraken/kraken.gif b/source_assets/joe/animations/kraken.gif
similarity index 100%
rename from source_assets/joe/sprites/kraken/kraken.gif
rename to source_assets/joe/animations/kraken.gif
diff --git a/source_assets/joe/sprites/shark/shark.gif b/source_assets/joe/animations/shark.gif
similarity index 100%
rename from source_assets/joe/sprites/shark/shark.gif
rename to source_assets/joe/animations/shark.gif
diff --git a/source_assets/joe/sprites/speedboat/speedboat.gif b/source_assets/joe/animations/speedboat.gif
similarity index 100%
rename from source_assets/joe/sprites/speedboat/speedboat.gif
rename to source_assets/joe/animations/speedboat.gif
diff --git a/source_assets/joe/sprites/sub/sub_side.gif b/source_assets/joe/animations/sub_side.gif
similarity index 100%
rename from source_assets/joe/sprites/sub/sub_side.gif
rename to source_assets/joe/animations/sub_side.gif
diff --git a/source_assets/joe/sprites/sub/sub_turn.gif b/source_assets/joe/animations/sub_turn.gif
similarity index 100%
rename from source_assets/joe/sprites/sub/sub_turn.gif
rename to source_assets/joe/animations/sub_turn.gif
diff --git a/source_assets/joe/animations/torpedo.gif b/source_assets/joe/animations/torpedo.gif
new file mode 100644
index 0000000000000000000000000000000000000000..68451094d341eb3c2f5a25b85ca8665271e294c8
Binary files /dev/null and b/source_assets/joe/animations/torpedo.gif differ
diff --git a/source_assets/joe/sprites/explosions/torpedo_explosion.gif b/source_assets/joe/animations/torpedo_explosion.gif
similarity index 100%
rename from source_assets/joe/sprites/explosions/torpedo_explosion.gif
rename to source_assets/joe/animations/torpedo_explosion.gif
diff --git a/source_assets/joe/config.yml b/source_assets/joe/config.yml
index c2a73c3a0460ba5afc17669b5a7cfa2a66cee6b9..d37ad331b9cace36ab5973a60c02aec498bfa49a 100644
--- a/source_assets/joe/config.yml
+++ b/source_assets/joe/config.yml
@@ -1,72 +1,104 @@
-theme_name: "Joe"
-palette_sets:
-  - { name: "p1", animation: "sub_side", palettes: ["p1", "p1", "p1", "p1"] }
-  - { name: "p2", animation: "sub_side", palettes: ["p2", "p2", "p2", "p2"] }
-  - { name: "low_air", animation: "sub_side", palettes: ["low_air", "", "", ""] }
-  - { name: "critical_air", animation: "sub_side", palettes: ["low_air", "", "critical_air", ""] }
-animations:
-  - { animation: "background", offset: [0, 0], max_colors: 255 }
-  - { animation: "diver", offset: [0, 0], max_colors: 12 }
-  - { animation: "kraken", offset: [6, 2], max_colors: 12 }
-  - { animation: "shark", offset: [14, 3], max_colors: 12 }
-  - { animation: "speedboat", offset: [0, 0], max_colors: 12 }
-  - { animation: "sub_side", offset: [9, 0], max_colors: 12 }
-  - { animation: "torpedo", offset: [0, 0], max_colors: 12 }
-  - { animation: "explosions", offset: [0, 0], max_colors: 12 }
-entities:
-  - {
-    name: "background",
-    mappings: [
-      { action: "background", sprite: "background", animation: "background" }]
-  }
-  - {
-    name: "player1",
-    mappings: [
-      { action: "idle", sprite: "sub", animation: "sub_side" },
-      { action: "move", sprite: "sub", animation: "sub_side"  },
-      { action: "fire", sprite: "sub", animation: "sub_side" },
-      { action: "death", sprite: "sub", animation: "sub_side" } ]
-  }
-  - {
-    name: "player2",
-    mappings: [
-      { action: "idle", sprite: "sub", animation: "sub_side" },
-      { action: "move", sprite: "sub", animation: "sub_side" },
-      { action: "fire", sprite: "sub", animation: "sub_side" },
-      { action: "death", sprite: "sub", animation: "sub_side" }]
-  }
-  - {
-    name: "patrol_sub",
-    mappings: [
-      { action: "idle", sprite: "speedboat", animation: "speedboat" },
-      { action: "move", sprite: "speedboat", animation: "speedboat" },
-      { action: "fire", sprite: "speedboat", animation: "speedboat" },
-      { action: "death", sprite: "speedboat", animation: "speedboat" }]
-  }
-  - {
-    name: "shark",
-    mappings: [
-      { action: "idle", sprite: "shark", animation: "shark"},
-      { action: "move", sprite: "shark", animation: "shark"},
-      { action: "fire", sprite: "shark", animation: "shark" },
-      { action: "death", sprite: "shark", animation: "shark"}]
-  }
-  - {
-    name: "bullet",
-    mappings: [
-      { action: "idle", sprite: "torpedo", animation: "torpedo", palette: "torpedo-default" },
-      { action: "collision", sprite: "explosions", animation: "torpedo_explosion", palette: "explosions-default" }
-    ]
-  }
-  - {
-    name: "diver",
-    mappings: [{ action: "idle", sprite: "diver", animation: "diver", palette: "diver-default" }]
-  }
-  - {
-    name: "sub",
-    mappings: [
-      { action: "idle", sprite: "kraken", animation: "kraken", palette: "kraken-default" },
-      { action: "move", sprite: "kraken", animation: "kraken", palette: "kraken-default" },
-      { action: "fire", sprite: "kraken", animation: "kraken", palette: "kraken-default" },
-      { action: "death", sprite: "kraken", animation: "kraken", palette: "kraken-default" }]
-  }
+{
+  name: "Joe",
+  required_mappings: [
+    { game_entity: "player1", animation_states: ["idle", "move"] },
+    { game_entity: "player2", animation_states: ["idle", "move"] },
+    { game_entity: "shark", animation_states: ["move"] },
+    { game_entity: "sub", animation_states: ["move"] },
+    { game_entity: "ink", animation_states: ["move"] },
+    { game_entity: "patrol_sub", animation_states: ["move"] },
+    { game_entity: "bullet", animation_states: ["move"] },
+    { game_entity: "diver", animation_states: ["move"] },
+    { game_entity: "background", animation_states: ["background"] },
+  ],
+  animations: [
+    {
+      source_animation: "sub_side",
+      offset: [ 9, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "player1", "player2" ],
+            animation_states: [ "idle", "move" ],
+          },
+    },
+    {
+      source_animation: "shark",
+      offset: [ 14, 3 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "shark" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "kraken",
+      offset: [ 6, 2 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "sub" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "ink",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "ink" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "speedboat",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "patrol_sub" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "torpedo",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "bullet" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "diver",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "diver" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "background",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 255,
+      game_entity_map:
+          {
+            game_entities: [ "background" ],
+            animation_states: [ "background" ],
+          },
+    },
+  ]
+}
diff --git a/source_assets/joe/sprites/shark/palette_set_0.palette.png b/source_assets/joe/sprites/shark/palette_set_0.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/shark/palette_set_0.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/shark/palette_set_1.palette.png b/source_assets/joe/sprites/shark/palette_set_1.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/shark/palette_set_1.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/shark/palette_set_2.palette.png b/source_assets/joe/sprites/shark/palette_set_2.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/shark/palette_set_2.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/shark/palette_set_3.palette.png b/source_assets/joe/sprites/shark/palette_set_3.palette.png
deleted file mode 100644
index 5b83903efbe0083c87fa775ce525a4fd3c847833..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/shark/palette_set_3.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/sub/critical_air.palette.png b/source_assets/joe/sprites/sub/critical_air.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/sub/critical_air.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/sub/low_air.palette.png b/source_assets/joe/sprites/sub/low_air.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/sub/low_air.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/sub/p1.palette.png b/source_assets/joe/sprites/sub/p1.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/sub/p1.palette.png and /dev/null differ
diff --git a/source_assets/joe/sprites/sub/p2.palette.png b/source_assets/joe/sprites/sub/p2.palette.png
deleted file mode 100644
index 87b8075313b332f2ada60fc5fcc2fca77751fc22..0000000000000000000000000000000000000000
Binary files a/source_assets/joe/sprites/sub/p2.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/background/background.gif b/source_assets/thorfinn/animations/background.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/background/background.gif
rename to source_assets/thorfinn/animations/background.gif
diff --git a/source_assets/thorfinn/sprites/bird/bird.gif b/source_assets/thorfinn/animations/bird.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/bird/bird.gif
rename to source_assets/thorfinn/animations/bird.gif
diff --git a/source_assets/thorfinn/sprites/cat/cat_idle.gif b/source_assets/thorfinn/animations/cat_idle.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/cat/cat_idle.gif
rename to source_assets/thorfinn/animations/cat_idle.gif
diff --git a/source_assets/thorfinn/sprites/cat/cat_shooting.gif b/source_assets/thorfinn/animations/cat_shooting.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/cat/cat_shooting.gif
rename to source_assets/thorfinn/animations/cat_shooting.gif
diff --git a/source_assets/thorfinn/sprites/croc/croc_spitting.gif b/source_assets/thorfinn/animations/croc_spitting.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/croc/croc_spitting.gif
rename to source_assets/thorfinn/animations/croc_spitting.gif
diff --git a/source_assets/thorfinn/sprites/dog/evil_dog.gif b/source_assets/thorfinn/animations/evil_dog.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/dog/evil_dog.gif
rename to source_assets/thorfinn/animations/evil_dog.gif
diff --git a/source_assets/thorfinn/sprites/mouse/mouse.gif b/source_assets/thorfinn/animations/mouse.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/mouse/mouse.gif
rename to source_assets/thorfinn/animations/mouse.gif
diff --git a/source_assets/thorfinn/sprites/yarn/yarn.gif b/source_assets/thorfinn/animations/yarn.gif
similarity index 100%
rename from source_assets/thorfinn/sprites/yarn/yarn.gif
rename to source_assets/thorfinn/animations/yarn.gif
diff --git a/source_assets/thorfinn/config.yml b/source_assets/thorfinn/config.yml
index 94e607ff743f9931bb6ff4efd82392f77f39b25e..2c9d93213315aab047f05072cce5ed5cd3c30e92 100644
--- a/source_assets/thorfinn/config.yml
+++ b/source_assets/thorfinn/config.yml
@@ -1,69 +1,93 @@
-theme_name: "thorfinn"
-palette_sets:
-  - { name: "p1", animation: "cat_shooting", palettes: ["p1", "p1", "p1", "p1"] }
-  - { name: "p2", animation: "cat_shooting", palettes: ["p2", "p2", "p2", "p2"] }
-  - { name: "low_air", animation: "cat_shooting", palettes: ["low_air", "", "", ""] }
-  - { name: "critical_air", animation: "cat_shooting", palettes: ["low_air", "", "critical_air", ""] }
-animations:
-  - { animation: "background", offset: [-53, 0] }
-  - { animation: "cat_idle", offset: [11, 11] }
-  - { animation: "cat_shooting", offset: [11, 11] }
-  - { animation: "bird", offset: [1, 3] }
-  - { animation: "croc_spitting", offset: [0, 10] }
-  - { animation: "evil_dog", offset: [6, 9] }
-  - { animation: "mouse", offset: [13, 13] }
-  - { animation: "yarn", offset: [7, 3] }
-entities:
-  - {
-    name: "background",
-    mappings: [
-      { action: "background", sprite: "background", animation: "background" }]
-  }
-  - {
-    name: "player1",
-    mappings: [
-      { action: "idle", sprite: "cat", animation: "cat_shooting", palette: "p1" },
-      { action: "move", sprite: "cat", animation: "cat_shooting", palette: "p1" },
-      { action: "fire", sprite: "cat", animation: "cat_shooting", palette: "p1", sound: "fire" },
-      { action: "death", sprite: "cat", animation: "cat_shooting", palette: "p1" } ]
-  }
-  - {
-    name: "player2",
-    mappings: [
-      { action: "idle", sprite: "cat", animation: "cat_shooting", palette: "p2" },
-      { action: "move", sprite: "cat", animation: "cat_shooting", palette: "p2" },
-      { action: "fire", sprite: "cat", animation: "cat_shooting", palette: "p2", sound: "fire" },
-      { action: "death", sprite: "cat", animation: "cat_shooting", palette: "p2" }]
-  }
-  - {
-    name: "sub",
-    mappings: [
-      { action: "idle", sprite: "dog", animation: "evil_dog", palette: "dog-default" },
-      { action: "move", sprite: "dog", animation: "evil_dog", palette: "dog-default" },
-      { action: "fire", sprite: "dog", animation: "evil_dog", palette: "dog-default", sound: "fire" },
-      { action: "death", sprite: "dog", animation: "evil_dog", palette: "dog-default" }]
-  }
-  - {
-    name: "shark",
-    mappings: [
-      { action: "idle", sprite: "croc", animation: "croc_spitting", palette: "croc-default" },
-      { action: "move", sprite: "croc", animation: "croc_spitting", palette: "croc-default" },
-      { action: "fire", sprite: "croc", animation: "croc_spitting", palette: "croc-default", sound: "fire" },
-      { action: "death", sprite: "croc", animation: "croc_spitting", palette: "croc-default" }]
-  }
-  - {
-    name: "bullet",
-    mappings: [{ action: "idle", sprite: "yarn", animation: "yarn", palette: "yarn-default" }]
-  }
-  - {
-    name: "diver",
-    mappings: [{ action: "idle", sprite: "mouse", animation: "mouse", palette: "mouse-default" }]
-  }
-  - {
-    name: "patrol_sub",
-    mappings: [
-      { action: "idle", sprite: "bird", animation: "bird", palette: "bird-default" },
-      { action: "move", sprite: "bird", animation: "bird", palette: "bird-default" },
-      { action: "fire", sprite: "bird", animation: "bird", palette: "bird-default", sound: "fire" },
-      { action: "death", sprite: "bird", animation: "bird", palette: "bird-default" }]
-  }
+{
+  name: "Thorfinn",
+  required_mappings: [
+    { game_entity: "player1", animation_states: ["idle", "move"] },
+    { game_entity: "player2", animation_states: ["idle", "move"] },
+    { game_entity: "shark", animation_states: ["move"] },
+    { game_entity: "sub", animation_states: ["move"] },
+    { game_entity: "ink", animation_states: ["move"] },
+    { game_entity: "patrol_sub", animation_states: ["move"] },
+    { game_entity: "bullet", animation_states: ["move"] },
+    { game_entity: "diver", animation_states: ["move"] },
+    { game_entity: "background", animation_states: ["background"] },
+  ],
+  animations: [
+    {
+      source_animation: "cat_idle",
+      offset: [ 11, 11 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "player1", "player2" ],
+            animation_states: [ "idle", "move" ],
+          },
+    },
+    {
+      source_animation: "croc_spitting",
+      offset: [ 0, 10 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "shark" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "evil_dog",
+      offset: [ 6, 9 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "sub" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "bird",
+      offset: [ 1, 3 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "patrol_sub" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "yarn",
+      offset: [ 7, 3 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "bullet" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "mouse",
+      offset: [ 0, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "diver" ],
+            animation_states: [ "move" ],
+          },
+    },
+    {
+      source_animation: "background",
+      offset: [ -53, 0 ],
+      looping: true,
+      max_colors: 12,
+      game_entity_map:
+          {
+            game_entities: [ "background" ],
+            animation_states: [ "background" ],
+          },
+    },
+  ]
+}
diff --git a/source_assets/thorfinn/sprites/cat/critical_air.palette.png b/source_assets/thorfinn/sprites/cat/critical_air.palette.png
deleted file mode 100644
index d335822bd59d863d4510ac0c6d9f641bd1a6f43b..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/cat/critical_air.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/cat/low_air.palette.png b/source_assets/thorfinn/sprites/cat/low_air.palette.png
deleted file mode 100644
index f3b61e389301ba54b2d6f33ab1a1aeeb4c17e110..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/cat/low_air.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/cat/p1.palette.png b/source_assets/thorfinn/sprites/cat/p1.palette.png
deleted file mode 100644
index a093ba8e4b079b6a15e61eafd9aece2d86d44818..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/cat/p1.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/cat/p2.palette.png b/source_assets/thorfinn/sprites/cat/p2.palette.png
deleted file mode 100644
index a093ba8e4b079b6a15e61eafd9aece2d86d44818..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/cat/p2.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/croc/croc_palette_0.palette.png b/source_assets/thorfinn/sprites/croc/croc_palette_0.palette.png
deleted file mode 100644
index cc900273591d183b3bdab26ec5afcc5d166a2bb7..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/croc/croc_palette_0.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/croc/croc_palette_1.palette.png b/source_assets/thorfinn/sprites/croc/croc_palette_1.palette.png
deleted file mode 100644
index cc900273591d183b3bdab26ec5afcc5d166a2bb7..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/croc/croc_palette_1.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/croc/croc_palette_2.palette.png b/source_assets/thorfinn/sprites/croc/croc_palette_2.palette.png
deleted file mode 100644
index cc900273591d183b3bdab26ec5afcc5d166a2bb7..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/croc/croc_palette_2.palette.png and /dev/null differ
diff --git a/source_assets/thorfinn/sprites/croc/croc_palette_3.palette.png b/source_assets/thorfinn/sprites/croc/croc_palette_3.palette.png
deleted file mode 100644
index cc900273591d183b3bdab26ec5afcc5d166a2bb7..0000000000000000000000000000000000000000
Binary files a/source_assets/thorfinn/sprites/croc/croc_palette_3.palette.png and /dev/null differ
diff --git a/src/config_parser.rs b/src/config_parser.rs
deleted file mode 100644
index fea955297ee1248a7c6302b1f5877ae2c611b534..0000000000000000000000000000000000000000
--- a/src/config_parser.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-use std::path::Path;
-use serde::{Deserialize, Serialize};
-use anyhow::Result;
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct PaletteSetEntry
-{
-    pub name: String,
-    pub animation: String,
-    pub palettes: Vec<String>,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct AnimationEntry
-{
-    pub animation: String,
-    pub offset: [f32; 2],
-    pub max_colors: Option<u8>,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct Mapping
-{
-    pub action: String,
-    pub sprite: Option<String>,
-    pub animation: Option<String>,
-    pub palette: Option<String>,
-    pub sound: Option<String>,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct Entity
-{
-    pub name: String,
-    pub mappings: Vec<Mapping>,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ThemeConfig
-{
-    pub theme_name: String,
-    pub palette_sets: Vec<PaletteSetEntry>,
-    pub animations: Vec<AnimationEntry>,
-    pub entities: Vec<Entity>,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ThemeAsset
-{
-    pub name: String,
-    pub path: String,
-    #[serde(rename = "type")]
-    pub type_name: String,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ThemeAnimation
-{
-    pub entity: String,
-    pub entity_anim: String,
-    pub source: String,
-    pub animation: String,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ThemeSound
-{
-    pub entity: String,
-    pub entity_sound: String,
-    pub source: String,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ThemeFile
-{
-    pub name: String,
-    pub assets: Vec<ThemeAsset>,
-    pub animations: Vec<ThemeAnimation>,
-    pub sounds: Vec<ThemeSound>,
-}
-
-pub fn load_theme_config(config_path: &Path) -> Result<ThemeConfig>
-{
-    let f = std::fs::File::open(config_path)?;
-    serde_yaml::from_reader::<std::fs::File, ThemeConfig>(f).map(|c| c).map_err(|e| e.into())
-}
diff --git a/src/gif_processing.rs b/src/gif_processing.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9147ff979c03787df28f0f2b912368df068e5dcc
--- /dev/null
+++ b/src/gif_processing.rs
@@ -0,0 +1,266 @@
+use std::path::PathBuf;
+use std::io::BufReader;
+
+
+use fxhash::FxHashMap;
+use image::{AnimationDecoder, DynamicImage, GrayImage, Luma};
+use kmeans_colors::{Calculate, get_kmeans, Sort};
+use palette::{Darken, FromColor, IntoColor, Lab, Lch, Lighten, ShiftHue, Srgba};
+use palette::white_point::D65;
+use rayon::prelude::*;
+use anyhow::Result;
+use crate::get_file_stem;
+
+pub type PaletteColors = Vec<Srgba<u8>>;
+pub type PaletteBrightnessVariations = Vec<PaletteColors>;
+pub type PaletteColorVariations = Vec<PaletteBrightnessVariations>;
+
+pub struct GifFrameInfo
+{
+    pub duration: f32,
+    pub frame: GrayImage,
+}
+pub struct ProcessedGif
+{
+    pub name: String,
+    pub source: PathBuf,
+    pub frames: Vec<GifFrameInfo>,
+    pub palette_variations: PaletteColorVariations,
+}
+
+struct HueModification
+{
+    starting_shift: f32,
+    shift_increment: f32,
+    num_variations: u32,
+}
+
+struct LightnessModification
+{
+    starting_lightness: f32,
+    lightness_increment: f32,
+    num_variations: u32,
+}
+
+struct DarknessModification
+{
+    starting_darkness: f32,
+    darkness_increment: f32,
+    num_variations: u32,
+}
+
+enum PaletteModification
+{
+    Hue(HueModification),
+    Lightness(LightnessModification),
+    Darkness(DarknessModification),
+}
+
+fn read_gif_frames(gif_path: &PathBuf) -> Result<Vec<(DynamicImage, f32)>>
+{
+    let gif_file = std::fs::File::open(gif_path)?;
+    let gif_file_buf = BufReader::new(gif_file);
+    let decoder = image::codecs::gif::GifDecoder::new(gif_file_buf)?;
+    let frames = decoder.into_frames().collect_frames()?;
+
+    Ok(frames.into_iter().map(|f| {
+        let delay = f.delay().numer_denom_ms();
+        let delay = (delay.0 as f32 / delay.1 as f32) * 0.001;
+        (DynamicImage::ImageRgba8(f.into_buffer()), delay)
+    }).collect::<Vec<_>>())
+}
+
+fn get_gif_lab_pixels(frames: &[(DynamicImage, f32)]) -> (Vec<Lab<D65, f32>>, Vec<(usize, usize)>)
+{
+    let mut lab_cache: FxHashMap<[u8; 3], Lab<D65, f32>> = FxHashMap::default();
+    let mut lab_pixels: Vec<Lab<D65, f32>> = Vec::new();
+    let mut frame_ranges = Vec::new();
+    let mut current_pixel: usize = 0;
+
+    for (frame, _delay) in frames
+    {
+        let starting_pixel  = current_pixel;
+        let pixels: Vec<_> = frame.to_rgba8().pixels().map(|p| Srgba::from(p.0)).collect();
+        lab_pixels.extend(pixels.iter().filter(|x: &&Srgba<u8>| x.alpha == 255).map(|color| {
+            current_pixel += 1;
+            *lab_cache.entry([color.red, color.green, color.blue])
+                .or_insert_with(|| color.into_linear::<_, f32>().into_color())
+        }));
+
+        frame_ranges.push((starting_pixel, current_pixel));
+    }
+
+    (lab_pixels, frame_ranges)
+}
+
+fn get_sorted_palette(lab_pixels: &[Lab<D65, f32>], max_colors: u8, runs: u32, iterations: usize) -> (Vec<Srgba<u8>>, Vec<u8>)
+{
+    let result = (0..runs).into_par_iter().map(|i| {
+        let run_result = get_kmeans(max_colors as usize, iterations, 0.0025, false, &lab_pixels, i as u64);
+        run_result
+    }).max_by(|a, b| a.score.partial_cmp(&b.score).unwrap()).unwrap();
+
+    let sorted_palette = Lab::<D65, f32>::sort_indexed_colors(&result.centroids, &result.indices);
+
+    let sorted_lab_palette = sorted_palette.iter().map(|c| c.centroid).collect::<Vec<_>>();
+    let sorted_rgb_palette =  sorted_palette.iter().map(|c| Srgba::from_format::<f32, u8>(c.centroid.into_color()));
+    let mut sorted_rgb_palette_with_alpha = vec![Srgba::<u8>::new(0, 0, 0, 0)];
+    sorted_rgb_palette_with_alpha.extend(sorted_rgb_palette);
+
+    let mut indices = Vec::new();
+    Lab::<D65, f32>::get_closest_centroid(&lab_pixels, &sorted_lab_palette, &mut indices);
+    indices.iter_mut().for_each(|i| *i += 1);
+
+    (sorted_rgb_palette_with_alpha, indices)
+}
+
+fn get_palette_variations(palette: &[Srgba<u8>], modification: PaletteModification) -> Vec<Vec<Srgba<u8>>>
+{
+    match modification
+    {
+        PaletteModification::Hue(hue_mod) =>
+            {
+                (0..hue_mod.num_variations).into_iter().map(|i| {
+                    let hue_shift = hue_mod.starting_shift + i as f32 * hue_mod.shift_increment;
+                    hue_shift_palette(palette, hue_shift)
+                }).collect()
+            }
+        PaletteModification::Lightness(lightness_mod) =>
+            {
+                (0..lightness_mod.num_variations).into_iter().map(|i| {
+                    let lightness = lightness_mod.starting_lightness + i as f32 * lightness_mod.lightness_increment;
+                    lighten_palette(palette, lightness)
+                }).collect()
+            }
+        PaletteModification::Darkness(darkness_mod) =>
+            {
+                (0..darkness_mod.num_variations).into_iter().map(|i| {
+                    let darkness = darkness_mod.starting_darkness + i as f32 * darkness_mod.darkness_increment;
+                    darken_palette(palette, darkness)
+                }).collect()
+            }
+    }
+}
+
+fn hue_shift_palette(palette: &[Srgba<u8>], hue_shift: f32) -> Vec<Srgba<u8>>
+{
+    palette.iter().map(|color| {
+        if color.alpha != 255
+        {
+            return *color;
+        }
+
+        let color: Lch = color.into_format::<f32, f32>().into_color();
+        let lch = color.shift_hue(hue_shift);
+        let srgba = Srgba::from_color(lch).into_format();
+        srgba
+    }).collect()
+}
+
+fn lighten_palette(palette: &[Srgba<u8>], amount: f32) -> Vec<Srgba<u8>>
+{
+    palette.iter().map(|color| {
+        if color.alpha != 255
+        {
+            return *color;
+        }
+
+        let color: Lch = color.into_format::<f32, f32>().into_color();
+        let lch = color.lighten(amount);
+        let srgba = Srgba::from_color(lch).into_format();
+        srgba
+    }).collect()
+}
+
+fn darken_palette(palette: &[Srgba<u8>], amount: f32) -> Vec<Srgba<u8>>
+{
+    palette.iter().map(|color| {
+        if color.alpha != 255
+        {
+            return *color;
+        }
+
+        let color: Lch = color.into_format::<f32, f32>().into_color();
+        let lch = color.darken(amount);
+        let srgba = Srgba::from_color(lch).into_format();
+        srgba
+    }).collect()
+}
+
+fn quantize_frame(frame: &DynamicImage, delay: f32, indices: &[u8], frame_indices_range: (usize, usize)) -> GifFrameInfo
+{
+    let pixels: Vec<_> = frame.to_rgba8().pixels().map(|p| Srgba::from(p.0)).collect();
+    let (start_index, end_index) = frame_indices_range;
+
+    let mut next_lab_pixel = 0;
+
+    let mut indexed_image = GrayImage::new(frame.width(), frame.height());
+
+    for (original, index_pixel) in pixels.iter().zip(indexed_image.pixels_mut())
+    {
+        let index = if original.alpha != 255
+        {
+            0
+        } else {
+            next_lab_pixel += 1;
+            let calculated_index = start_index + next_lab_pixel - 1;
+            debug_assert!(calculated_index < end_index, "Index out of range: {} {} {}", start_index, next_lab_pixel, end_index);
+            indices[calculated_index]
+        };
+
+        *index_pixel = Luma([index]);
+    }
+
+    GifFrameInfo { frame: indexed_image, duration: delay }
+}
+
+fn quantize_frames(frames: Vec<(DynamicImage, f32)>, max_colors: u8) -> (Vec<GifFrameInfo>, PaletteColorVariations)
+{
+    let (lab_pixels, frame_indices_ranges) = get_gif_lab_pixels(&frames);
+    let (sorted_palette, indices) = get_sorted_palette(&lab_pixels, max_colors, 10, 10);
+    let hue_variations = get_palette_variations(&sorted_palette, PaletteModification::Hue(HueModification {
+        starting_shift: 0.0,
+        shift_increment: 20.0,
+        num_variations: 20,
+    }));
+
+    let all_variations = hue_variations.iter().map(|palette| {
+        let mut new_palettes = Vec::new();
+        // This one counts backwards because the palettes are ordered from light to dark
+        let lightness_variations = get_palette_variations(palette, PaletteModification::Lightness(LightnessModification {
+            starting_lightness: 0.2,
+            lightness_increment: -0.1,
+            num_variations: 2,
+        }));
+
+        let darkness_variations = get_palette_variations(palette, PaletteModification::Darkness(DarknessModification {
+            starting_darkness: 0.15,
+            darkness_increment: 0.15,
+            num_variations: 2,
+        }));
+
+        // Order the palettes from light to dark
+        new_palettes.extend(lightness_variations.into_iter());
+        new_palettes.extend([palette.clone()]);
+        new_palettes.extend(darkness_variations.into_iter());
+
+        new_palettes
+
+    }).collect::<Vec<_>>();
+
+    let quantized_frames = frames.par_iter().enumerate().map(|(i, (frame, delay))|
+    {
+        quantize_frame(&frame, *delay, &indices, frame_indices_ranges[i])
+    }).collect::<Vec<_>>();
+
+    (quantized_frames, all_variations)
+}
+
+pub fn process_gif(gif_path: &PathBuf, max_colors: u8) -> Result<ProcessedGif>
+{
+    let name = get_file_stem(gif_path)?;
+    let frames = read_gif_frames(gif_path)?;
+    let (frames, palette_variations) = quantize_frames(frames, max_colors);
+
+    Ok(ProcessedGif { name, source: gif_path.clone(), frames, palette_variations })
+}
diff --git a/src/main.rs b/src/main.rs
index 275dea1871bc8fbec127d9cf0f7c5c55abc74234..fedade32725fbc3271d09255a126a8c809d8d9a2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,332 +1,19 @@
-mod config_parser;
+mod theme_config_parser;
 mod sprite_parser;
+mod theme_parser;
+mod gif_processing;
 
 use image::*;
 use std::path::PathBuf;
 use std::fs;
-use std::fs::{create_dir_all, File};
-use std::io::Write;
+use std::fs::{create_dir_all, };
 use rayon::prelude::*;
-use walkdir::*;
 use anyhow::{anyhow, Result};
-use fxhash::{FxHashMap};
-use image::{AnimationDecoder, DynamicImage, Rgba, RgbaImage};
-use kmeans_colors::{Calculate, get_kmeans, Sort};
-use palette::{white_point::D65, IntoColor, Lab, Srgba, ShiftHue, Lch, FromColor, Darken, Lighten};
-use crate::config_parser::{ThemeAnimation, ThemeAsset, ThemeConfig, ThemeFile, ThemeSound};
-use crate::sprite_parser::{PaletteSet, Sprite, SpriteAnimation, SpriteAnimationFrame, SpriteMaterial, SpritePalette, SpritePalettePath, SpriteParse, SpriteTexture};
-
-type PaletteColors = Vec<Srgba<u8>>;
-type PaletteBrightnessVariations = Vec<PaletteColors>;
-type PaletteColorVariations = Vec<PaletteBrightnessVariations>;
-
-fn log_error<T>(result: &Result<T>)
-{
-    match result
-    {
-        Ok(_) => {}
-        Err(e) => println!("{}", e),
-    }
-}
-
-// fn sprite_parse_to_yaml(sprite_parse: &SpriteParse) -> Result<String>
-// {
-//     Ok(serde_yaml::to_string(sprite_parse)?)
-// }
-//
-// fn sprite_parse_to_toml(sprite_parse: &SpriteParse) -> Result<String>
-// {
-//     Ok(toml::Value::try_from(sprite_parse)?.to_string())
-// }
-
-fn get_file_stem(path: &PathBuf) -> Result<String>
-{
-    if let Some(filename) = path.file_stem()
-    {
-        if let Some(filename) = filename.to_str()
-        {
-            return Ok(filename.into());
-        }
-    }
-
-    Err(anyhow!("Failed to get filename from: {}", path.to_str().unwrap_or("Failed to convert path to string")))
-}
-
-fn get_final_path_component(path: &PathBuf) -> Result<String>
-{
-    if let Some(filename) = path.file_name()
-    {
-        if let Some(filename) = filename.to_str()
-        {
-            return Ok(filename.into());
-        }
-    }
-
-    Err(anyhow!("Failed to get filename from: {}", path.to_str().unwrap_or("Failed to convert path to string")))
-}
-
-fn get_theme_dirs(root: &PathBuf) -> Vec<PathBuf>
-{
-    fs::read_dir(root).unwrap().filter_map(|e| e.ok()).filter_map(|e| {
-        let path = e.path();
-        if path.is_dir() {
-            Some(path)
-        } else {
-            None
-        }
-    }).collect()
-}
-
-fn get_theme_gif_paths(theme_dir: &PathBuf) -> Vec<PathBuf>
-{
-    WalkDir::new(theme_dir).into_iter().filter_map(|e| e.ok()).filter_map(|e| {
-        let path = e.path();
-        if path.is_file() && path.extension().unwrap_or_default() == "gif" {
-            Some(path.to_path_buf())
-        } else {
-            None
-        }
-    }).collect()
-}
-
-fn read_gif_frames(gif_path: &PathBuf) -> Result<Vec<(DynamicImage, f32)>>
-{
-    let gif = std::fs::File::open(gif_path)?;
-    let decoder = image::codecs::gif::GifDecoder::new(gif)?;
-    let frames = decoder.into_frames().collect_frames()?;
-
-    Ok(frames.into_iter().map(|f| {
-        let delay = f.delay().numer_denom_ms();
-        let delay = (delay.0 as f32 / delay.1 as f32) * 0.001;
-        (DynamicImage::ImageRgba8(f.into_buffer()), delay)
-    }).collect::<Vec<_>>())
-}
-
-fn get_gif_lab_pixels(frames: &[(DynamicImage, f32)]) -> (Vec<Lab<D65, f32>>, Vec<(usize, usize)>)
-{
-    let mut lab_cache: FxHashMap<[u8; 3], Lab<D65, f32>> = FxHashMap::default();
-    let mut lab_pixels: Vec<Lab<D65, f32>> = Vec::new();
-    let mut frame_ranges = Vec::new();
-    let mut current_pixel: usize = 0;
-
-    for (frame, _delay) in frames
-    {
-        let starting_pixel  = current_pixel;
-        let pixels: Vec<_> = frame.to_rgba8().pixels().map(|p| Srgba::from(p.0)).collect();
-        lab_pixels.extend(pixels.iter().filter(|x: &&Srgba<u8>| x.alpha == 255).map(|color| {
-            current_pixel += 1;
-            *lab_cache.entry([color.red, color.green, color.blue])
-                .or_insert_with(|| color.into_linear::<_, f32>().into_color())
-        }));
-
-        frame_ranges.push((starting_pixel, current_pixel));
-    }
-
-    (lab_pixels, frame_ranges)
-}
-
-fn get_sorted_palette(lab_pixels: &[Lab<D65, f32>], max_colors: u8, runs: u32, iterations: usize) -> (Vec<Srgba<u8>>, Vec<u8>)
-{
-    let result = (0..runs).into_par_iter().map(|i| {
-        let run_result = get_kmeans(max_colors as usize, iterations, 0.0025, false, &lab_pixels, i as u64);
-        run_result
-    }).max_by(|a, b| a.score.partial_cmp(&b.score).unwrap()).unwrap();
-
-    let sorted_palette = Lab::<D65, f32>::sort_indexed_colors(&result.centroids, &result.indices);
-
-    let sorted_lab_palette = sorted_palette.iter().map(|c| c.centroid).collect::<Vec<_>>();
-    let sorted_rgb_palette =  sorted_palette.iter().map(|c| Srgba::from_format::<f32, u8>(c.centroid.into_color()));
-    let mut sorted_rgb_palette_with_alpha = vec![Srgba::<u8>::new(0, 0, 0, 0)];
-    sorted_rgb_palette_with_alpha.extend(sorted_rgb_palette);
-
-    let mut indices = Vec::new();
-    Lab::<D65, f32>::get_closest_centroid(&lab_pixels, &sorted_lab_palette, &mut indices);
-    indices.iter_mut().for_each(|i| *i += 1);
-
-    (sorted_rgb_palette_with_alpha, indices)
-}
-
-fn write_palette_to_file(filename: &PathBuf, palette: &[Srgba<u8>]) -> Result<()>
-{
-    let mut image = RgbaImage::new(palette.len() as u32, 1);
-    image.pixels_mut().zip(palette).par_bridge().for_each(|(pixel, original)|
-    {
-        *pixel = Rgba((*original).into());
-    });
-
-    image.save(filename)?;
-    Ok(())
-}
-
-struct HueModification
-{
-    starting_shift: f32,
-    shift_increment: f32,
-    num_variations: u32,
-}
-
-struct LightnessModification
-{
-    starting_lightness: f32,
-    lightness_increment: f32,
-    num_variations: u32,
-}
-
-struct DarknessModification
-{
-    starting_darkness: f32,
-    darkness_increment: f32,
-    num_variations: u32,
-}
-
-enum PaletteModification
-{
-    Hue(HueModification),
-    Lightness(LightnessModification),
-    Darkness(DarknessModification),
-}
-
-fn get_palette_variations(palette: &[Srgba<u8>], modification: PaletteModification) -> Vec<Vec<Srgba<u8>>>
-{
-    match modification
-    {
-        PaletteModification::Hue(hue_mod) =>
-            {
-                (0..hue_mod.num_variations).into_iter().map(|i| {
-                    let hue_shift = hue_mod.starting_shift + i as f32 * hue_mod.shift_increment;
-                    hue_shift_palette(palette, hue_shift)
-                }).collect()
-            }
-        PaletteModification::Lightness(lightness_mod) =>
-            {
-                (0..lightness_mod.num_variations).into_iter().map(|i| {
-                    let lightness = lightness_mod.starting_lightness + i as f32 * lightness_mod.lightness_increment;
-                    lighten_palette(palette, lightness)
-                }).collect()
-            }
-        PaletteModification::Darkness(darkness_mod) =>
-            {
-                (0..darkness_mod.num_variations).into_iter().map(|i| {
-                    let darkness = darkness_mod.starting_darkness + i as f32 * darkness_mod.darkness_increment;
-                    darken_palette(palette, darkness)
-                }).collect()
-            }
-    }
-}
-
-fn hue_shift_palette(palette: &[Srgba<u8>], hue_shift: f32) -> Vec<Srgba<u8>>
-{
-    palette.iter().map(|color| {
-        if color.alpha != 255
-        {
-            return *color;
-        }
-
-        let color: Lch = color.into_format::<f32, f32>().into_color();
-        let lch = color.shift_hue(hue_shift);
-        let srgba = Srgba::from_color(lch).into_format();
-        srgba
-    }).collect()
-}
-
-fn lighten_palette(palette: &[Srgba<u8>], amount: f32) -> Vec<Srgba<u8>>
-{
-    palette.iter().map(|color| {
-        if color.alpha != 255
-        {
-            return *color;
-        }
-
-        let color: Lch = color.into_format::<f32, f32>().into_color();
-        let lch = color.lighten(amount);
-        let srgba = Srgba::from_color(lch).into_format();
-        srgba
-    }).collect()
-}
-
-fn darken_palette(palette: &[Srgba<u8>], amount: f32) -> Vec<Srgba<u8>>
-{
-    palette.iter().map(|color| {
-        if color.alpha != 255
-        {
-            return *color;
-        }
-
-        let color: Lch = color.into_format::<f32, f32>().into_color();
-        let lch = color.darken(amount);
-        let srgba = Srgba::from_color(lch).into_format();
-        srgba
-    }).collect()
-}
-
-fn quantize_frame(frame: &DynamicImage, delay: f32, indices: &[u8], frame_indices_range: (usize, usize)) -> GifFrameInfo
-{
-    let pixels: Vec<_> = frame.to_rgba8().pixels().map(|p| Srgba::from(p.0)).collect();
-    let (start_index, end_index) = frame_indices_range;
-
-    let mut next_lab_pixel = 0;
-
-    let mut indexed_image = GrayImage::new(frame.width(), frame.height());
-
-    for (original, index_pixel) in pixels.iter().zip(indexed_image.pixels_mut())
-    {
-        let index = if original.alpha != 255
-        {
-            0
-        } else {
-            next_lab_pixel += 1;
-            let calculated_index = start_index + next_lab_pixel - 1;
-            debug_assert!(calculated_index < end_index, "Index out of range: {} {} {}", start_index, next_lab_pixel, end_index);
-            indices[calculated_index]
-        };
-
-        *index_pixel = Luma([index]);
-    }
-
-    GifFrameInfo { frame: indexed_image, duration: delay }
-}
-
-fn quantize_frames(frames: Vec<(DynamicImage, f32)>, max_colors: u8) -> (Vec<GifFrameInfo>, PaletteColorVariations)
-{
-    let (lab_pixels, frame_indices_ranges) = get_gif_lab_pixels(&frames);
-    let (sorted_palette, indices) = get_sorted_palette(&lab_pixels, max_colors, 10, 10);
-    let hue_variations = get_palette_variations(&sorted_palette, PaletteModification::Hue(HueModification {
-        starting_shift: 0.0,
-        shift_increment: 20.0,
-        num_variations: 20,
-    }));
-
-    let all_variations = hue_variations.iter().map(|palette| {
-        let mut new_palettes = Vec::new();
-        // This one counts backwards because the palettes are ordered from light to dark
-        let lightness_variations = get_palette_variations(palette, PaletteModification::Lightness(LightnessModification {
-            starting_lightness: 0.2,
-            lightness_increment: -0.1,
-            num_variations: 2,
-        }));
-
-        let darkness_variations = get_palette_variations(palette, PaletteModification::Darkness(DarknessModification {
-            starting_darkness: 0.15,
-            darkness_increment: 0.15,
-            num_variations: 2,
-        }));
-
-        // Order the palettes from light to dark
-        new_palettes.extend(lightness_variations.into_iter());
-        new_palettes.extend([palette.clone()]);
-        new_palettes.extend(darkness_variations.into_iter());
-
-        new_palettes
-
-    }).collect::<Vec<_>>();
-
-    let quantized_frames = frames.par_iter().enumerate().map(|(i, (frame, delay))|
-    {
-        quantize_frame(&frame, *delay, &indices, frame_indices_ranges[i])
-    }).collect::<Vec<_>>();
-
-    (quantized_frames, all_variations)
-}
+use image::{Rgba, RgbaImage};
+use palette::{Srgba};
+use crate::theme_config_parser::{ThemeConfig};
+use crate::gif_processing::*;
+use crate::theme_parser::{generate_theme_file, serialize_theme_file};
 
 #[derive(Clone)]
 struct ImageRect
@@ -362,6 +49,7 @@ struct SpriteSheet
     image: GrayImage,
     group_name: String,
     spacing: u16,
+    filename: PathBuf,
 }
 
 impl SpriteSheet
@@ -380,6 +68,7 @@ impl SpriteSheet
             frame_mappings: Vec::new(),
             image: GrayImage::new(max_dimensions as u32, max_dimensions as u32),
             spacing,
+            filename: PathBuf::new(),
         }
     }
 
@@ -492,12 +181,13 @@ fn generate_sprite_sheets(destination_dir: &PathBuf, group_name: &str, max_dimen
     // Crop all sprite sheets to size
     sprite_sheets.iter_mut().enumerate().for_each(|(i, sheet)|{
         sheet.image = sheet.image.view(0, 0, sheet.width as u32, sheet.height as u32).to_image();
-        let path = destination_dir.join(group_name);
-        let file_name = make_texture_name(&sheet.group_name, i as u16);
+        let path = destination_dir;
+        let file_name = format!("{}-sheet-{}.indexed", &sheet.group_name, i as u16);
         let file_path = path.join(format!("{}.png", file_name));
 
         create_dir_all(&path).expect("Failed to create sprite sheet directory");
-        sheet.image.save(file_path).expect("Failed to save sprite sheet image");
+        sheet.image.save(&file_path).expect("Failed to save sprite sheet image");
+        sheet.filename = file_path;
     });
 
     sprite_sheets.iter_mut().for_each(|sprite_sheet| {
@@ -516,23 +206,11 @@ fn generate_sprite_sheets(destination_dir: &PathBuf, group_name: &str, max_dimen
     Ok(sprite_sheets)
 }
 
-struct GifFrameInfo
-{
-    duration: f32,
-    frame: GrayImage,
-}
-struct ProcessedGif
-{
-    name: String,
-    source: PathBuf,
-    frames: Vec<GifFrameInfo>,
-    palette_variations_index: u16,
-}
-
-fn render_color_sprite_sheet(group_name: &str, destination_dir: &PathBuf, sprite_sheet: &SpriteSheet, palettes: &Vec<PaletteColorVariations>) -> Result<()>
+fn render_color_sprite_sheet(destination_dir: &PathBuf, sprite_sheet: &SpriteSheet, palettes: &PaletteColorVariations) -> Result<PathBuf>
 {
     let mut image = RgbaImage::new(sprite_sheet.image.width(), sprite_sheet.image.height());
 
+    let group_name = &sprite_sheet.group_name;
     let sprite_sheet_index = sprite_sheet.index;
     create_dir_all(destination_dir)?;
     let file_name = format!("{group_name}-sheet-{sprite_sheet_index}_default.png");
@@ -546,7 +224,7 @@ fn render_color_sprite_sheet(group_name: &str, destination_dir: &PathBuf, sprite
 
         let mut destination = image.sub_image(x as u32, y as u32, width as u32, height as u32);
         let source = sprite_sheet.image.view(x as u32, y as u32, width as u32, height as u32);
-        let palette = &palettes[frame_mapping.gif_index as usize][0][2];
+        let palette = &palettes[0][2];
         for (x, y, src_pixel) in source.pixels()
         {
             let index = src_pixel[0] as usize;
@@ -556,439 +234,93 @@ fn render_color_sprite_sheet(group_name: &str, destination_dir: &PathBuf, sprite
         }
     }
 
-    image.save(path)?;
-
-    Ok(())
-}
-
-struct ThemeSerializationConfig
-{
-    theme_name: String,
-    group_name: String,
-    config: ThemeConfig,
-    destination_images_dir: PathBuf,
-    destination_sprites_dir: PathBuf,
-
-    sprite_sheets: Vec<SpriteSheet>,
-    palettes: Vec<PaletteColorVariations>,
-}
-
-fn generate_theme_config(destination_theme_dir: &PathBuf, config: &ThemeConfig, sprite_file_paths: &[SpriteFile]) -> Result<()>
-{
-    let assets = sprite_file_paths.iter().map(|SpriteFile { name, path }|
-    {
-        let path = path.to_str().expect("Failed to convert path to string").to_string();
-
-        Ok(ThemeAsset
-        {
-            name: name.clone(),
-            path,
-            type_name: "sprite".to_string()
-        })
-    })
-        .inspect(log_error)
-        .flatten()
-        .collect::<Vec<_>>();
-
-    let animations = config.entities.iter().flat_map(|entity|
-    {
-        entity.mappings.iter().flat_map(|mapping|
-        {
-            match (&mapping.sprite, &mapping.animation)
-            {
-                (Some(sprite), Some(animation)) => Some(ThemeAnimation
-                {
-                    entity: entity.name.clone(),
-                    entity_anim: mapping.action.clone(),
-                    source: sprite.clone(),
-                    animation: animation.clone()
-                }),
-                _ => None
-            }
-        })
-    })
-        .collect::<Vec<_>>();
-
-    let sounds = config.entities.iter().flat_map(|entity|
-    {
-        entity.mappings.iter().flat_map(|mapping|
-        {
-            mapping.sound.as_ref().map(|sound|
-            {
-                ThemeSound
-                {
-                    entity: entity.name.clone(),
-                    entity_sound: mapping.action.clone(),
-                    source: sound.clone()
-                }
-            })
-        })
-    })
-        .collect::<Vec<_>>();
+    image.save(&path)?;
 
-    let theme_data = ThemeFile
-    {
-        name: config.theme_name.clone(),
-        assets,
-        animations,
-        sounds,
-    };
-
-    let destination_theme_file_path = destination_theme_dir.join("theme.toml");
-    //println!("Theme file: {:?}", theme_data);
-    println!("Writing theme file: {:?}", destination_theme_file_path);
-    let mut theme_file = File::create(destination_theme_file_path)?;
-    Ok(theme_file.write_all(toml::to_string(&theme_data)?.as_bytes())?)
+    Ok(path)
 }
 
-fn process_theme(source_dir: &PathBuf, destination_dir: &PathBuf, max_dimensions: u16) -> Result<()>
+fn process_theme(theme_dir: &PathBuf, destination_dir: &PathBuf, max_dimensions: u16) -> Result<()>
 {
-    let sprite_dirs = get_theme_dirs(&source_dir.join("sprites"));
-    let theme_config_path = source_dir.join("config.yml");
-    let theme_config = config_parser::load_theme_config(&theme_config_path)?;
-    let theme_name = get_final_path_component(source_dir)?;
-    let theme_destination_dir = destination_dir.join(&theme_name);
-    let theme_images_destination_dir = theme_destination_dir.join("images");
-    let theme_sprite_file_destination_dir = theme_destination_dir.join("sprites");
-    let render_destination_dir= theme_destination_dir.join("renders");
-    //dbg!(&sprite_dirs, &theme_name, &theme_images_destination_dir, &theme_sprite_file_destination_dir);
-
-    let sprite_paths = sprite_dirs.par_iter().map(|sprite_dir| {
-        let gif_paths = get_theme_gif_paths(sprite_dir);
-        fn process_gif(gif_path: &PathBuf, max_colors: u8) -> Result<(ProcessedGif, Vec<PaletteBrightnessVariations>)>
-        {
-            let name = get_file_stem(gif_path)?;
-            let frames = read_gif_frames(gif_path)?;
-            let (frames, palette_variations) = quantize_frames(frames, max_colors);
-
-            Ok((ProcessedGif { name, source: gif_path.clone(), frames, palette_variations_index: 0 }, palette_variations))
-        }
-
-        let anim_group_name = get_final_path_component(sprite_dir)?;
-        let max_colors = theme_config.animations.iter().find_map(|animation| {
-            if animation.animation == anim_group_name
-            {
-                animation.max_colors
-            } else {
-                None
-            }
-        }).unwrap_or(15);
-
-        let mut process_gifs_results = gif_paths.par_iter().filter_map(|g| {
-            process_gif(g, max_colors).ok()
-        }).collect::<Vec<_>>();
-
-        process_gifs_results.sort_by(|a, b| b.0.frames[0].frame.width().cmp(&a.0.frames[0].frame.width()));
+    let theme_config_path = theme_dir.join("config.yml");
+    let theme_config = load_theme_config(&theme_config_path)?;
 
-        let mut all_palette_variations = Vec::new();
-        let mut processed_gifs = Vec::new();
+    dbg!(&theme_config.name);
 
-        // TODO: Deduplicate palette variations and update palette indices
-        for (mut processed_gif, palette_variations) in process_gifs_results
-        {
-            all_palette_variations.push(palette_variations);
-            processed_gif.palette_variations_index = all_palette_variations.len() as u16 - 1;
-            processed_gifs.push(processed_gif);
-        }
-
-        let group_name = get_final_path_component(sprite_dir)?;
-        let sprite_sheets = generate_sprite_sheets(&theme_images_destination_dir, &group_name, max_dimensions, &processed_gifs)?;
-        sprite_sheets.iter().for_each(|sprite_sheet| render_color_sprite_sheet(&group_name, &render_destination_dir, sprite_sheet, &all_palette_variations).expect("Failed to render sprite sheet"));
-
-        let theme_serialization_config = ThemeSerializationConfig {
-            theme_name: theme_name.clone(),
-            group_name,
-            config: theme_config.clone(),
-            destination_images_dir: theme_images_destination_dir.clone(),
-            destination_sprites_dir: theme_sprite_file_destination_dir.clone(),
-            sprite_sheets,
-            palettes: all_palette_variations,
-        };
-
-        generate_sprite_files(&theme_serialization_config, &processed_gifs)
-    }).inspect(log_error)
-        .flatten()
-        .collect::<Vec<_>>();
+    let animations_sprites = theme_config.animations.iter().map(|anim| anim.source_animation.clone()).collect::<Vec<_>>();
+    dbg!(&animations_sprites);
 
-    generate_theme_config(&theme_destination_dir, &theme_config, &sprite_paths)
-}
+    let theme_file = generate_theme_file(&theme_config, &theme_dir, &destination_dir, max_dimensions)?;
 
-struct SpriteFile
-{
-    name: String,
-    path: PathBuf,
-}
+    let theme_file_destination = destination_dir.join(&theme_config.name);
+    create_dir_all(&theme_file_destination)?;
+    serialize_theme_file(&theme_file, &theme_file_destination)?;
 
-fn generate_sprite_files(theme_config: &ThemeSerializationConfig, processed_gifs: &[ProcessedGif]) -> Result<SpriteFile>
-{
-    //dbg!(theme_config);
-    //let gif_paths = processed_gifs.iter().map(|gif| gif.source.clone()).collect::<Vec<_>>();
-    //let sprite_names = processed_gifs.iter().filter_map(|gif| get_file_stem(&gif.source).ok()).collect::<Vec<_>>();
-    //dbg!(gif_paths, sprite_names);
-
-    let sprite_textures = get_sprite_textures(theme_config.theme_name.as_str(), theme_config.group_name.as_str(), &theme_config.sprite_sheets);
-    //dbg!(&sprite_textures);
-
-    let sprites = get_sprites(&theme_config);
-    //dbg!(&sprites);
-
-    let materials = get_materials(theme_config.group_name.as_str(), &theme_config.sprite_sheets);
-    //dbg!(&materials);
-
-    let (palette_files, palettes) = write_palettes(&theme_config, processed_gifs);
-    //dbg!(&palettes);
-
-    let animations = get_animations(&theme_config);
-    //dbg!(&animations);
-
-    let sprite_parse = SpriteParse
-    {
-        name: theme_config.group_name.clone(),
-        textures: sprite_textures,
-        sprites,
-        materials,
-        palette_files,
-        animations,
-        palettes,
-    };
-
-    //dbg!(&sprite_parse);
-
-    let toml = toml::to_string(&sprite_parse)?;
-    let destination_dir = theme_config.destination_sprites_dir.join(&theme_config.group_name);
-    let destination = destination_dir.join(format!("{}.toml", sprite_parse.name));
-    //dbg!(&destination);
-
-    create_dir_all(destination_dir)?;
-    let mut file = File::create(&destination)?;
-    file.write_all(toml.as_bytes())?;
-
-    Ok(SpriteFile { name: sprite_parse.name, path: destination })
-}
-
-fn make_material_name(group_name: &str, sprite_sheet_index: u16) -> String
-{
-    let sprite_name = format!("{group_name}-sheet-{sprite_sheet_index}.indexed");
-    let material_name = format!("{}_material_{}", sprite_name, sprite_sheet_index);
-
-    material_name
+    Ok(())
 }
 
-fn make_texture_name(group_name: &str, sprite_sheet_index: u16) -> String
+fn load_theme_config(theme_config_path: &PathBuf) -> Result<ThemeConfig>
 {
-    format!("{group_name}-sheet-{sprite_sheet_index}.indexed")
+    theme_config_parser::load_theme_config(theme_config_path)
 }
 
-fn make_sprite_name(group_name: &str, animation_index: u32, frame_index: u16) -> String
+fn main() -> Result<()>
 {
-    format!("{group_name}_{animation_index}_{frame_index}")
-}
+    // Inputs
+    let max_dimension = 1024;
+    let source_dir = PathBuf::from("./source_assets/");
+    let theme_dirs = get_theme_dirs(&source_dir);
+    //let theme_dirs = [PathBuf::from("./source_assets/fallback")];
+    theme_dirs.par_iter().map(|theme| process_theme(theme, &"./themes".into(), max_dimension)) //{ process_theme(theme, &"./themes".into(), max_dimension) })
+        .inspect(log_error)
+        .flatten()
+        .for_each(|_| {});
 
-fn make_palette_name(group_name: &str, color_variation_index: u16, brightness_index: u16) -> String
-{
-    format!("{group_name}-palette-{color_variation_index}_{brightness_index}")
+    Ok(())
 }
 
-fn write_palettes(theme_config: &ThemeSerializationConfig, processed_gifs: &[ProcessedGif]) -> (Vec<SpritePalettePath>, Vec<SpritePalette>)
+fn log_error<T>(result: &Result<T>)
 {
-    let mut palette_paths = Vec::new();
-    let mut palette_index = 1;
-    let mut sprite_palettes = Vec::new();
-
-    for (gif_index, processed_gif) in processed_gifs.iter().enumerate()
+    match result
     {
-        let group_name = theme_config.group_name.clone();
-        let default_palettes_name = format!("{}-default", processed_gif.name);
-        let default_palettes_path = theme_config.destination_images_dir.join(&format!("{group_name}/{default_palettes_name}.palette.png"));
-        let path_dir = theme_config.destination_images_dir.join(&format!("{group_name}"));
-        create_dir_all(&path_dir).ok();
-
-        // TODO: Exclude unwanted palettes somehow
-        palette_paths.push(SpritePalettePath
-        {
-            name: default_palettes_name,
-            path: default_palettes_path.to_str().expect("Failed to convert path to string").to_string(),
-        });
-
-        let color_index = processed_gif.palette_variations_index as usize;
-        write_palette_to_file(&default_palettes_path, &theme_config.palettes[gif_index][color_index][2]).ok();
-
-        palette_paths.extend(theme_config.palettes.iter().enumerate().flat_map(|(_, palette_variations)|
-        {
-            palette_variations.iter().enumerate().flat_map(|(i, brightness)|
-            {
-                let mut sprite_palette = SpritePalette
-                {
-                    name: format!("palette_{i}").to_string(),
-                    default_variation: palette_index + 2, // The middle variation is 2 after the first
-                    variations: Vec::new()
-                };
-
-                let variations = brightness.iter().enumerate().map(|(j, brightness_palette)|
-                {
-                    let name = make_palette_name(&group_name, i as u16, j as u16);
-                    let path_dir = path_dir.join(&format!("palettes/{}", sprite_palette.name));
-                    create_dir_all(&path_dir).ok();
-
-                    let path = path_dir.join(&format!("{name}.palette.png"));
-
-                    write_palette_to_file(&path, brightness_palette)?;
-
-                    sprite_palette.variations.push(palette_index);
-                    palette_index += 1;
-
-                    Ok(SpritePalettePath
-                    {
-                        name,
-                        path: path.to_str().expect("Failed to convert path to string").to_string(),
-                    })
-                }).collect::<Vec<_>>();
-
-                sprite_palettes.push(sprite_palette);
-
-                variations
-            }).collect::<Vec<_>>()
-        }).inspect(log_error).flatten().collect::<Vec<_>>());
+        Ok(_) => {}
+        Err(e) => println!("{}", e),
     }
-
-    (palette_paths, sprite_palettes)
 }
 
-fn get_materials(group_name: &str, sprite_sheets: &[SpriteSheet]) -> Vec<SpriteMaterial>
+fn get_file_stem(path: &PathBuf) -> Result<String>
 {
-    sprite_sheets.iter().map(|sheet| {
-        let name = make_material_name(group_name, sheet.index as u16);
-        let texture_name = make_texture_name(group_name, sheet.index as u16);
-        SpriteMaterial
+    if let Some(filename) = path.file_stem()
+    {
+        if let Some(filename) = filename.to_str()
         {
-            name,
-            texture_name,
+            return Ok(filename.into());
         }
-    }).collect()
-}
-
-fn get_sprite_textures(theme_name: &str, animation_group_name: &str, sprite_sheets: &[SpriteSheet]) -> Vec<SpriteTexture>
-{
-    sprite_sheets.iter().flat_map(|sprite_sheet| sprite_sheet.frame_mappings.iter().map(|_frame_mapping|
-        {
-            let sprite_sheet_index = sprite_sheet.index;
-            let name = make_texture_name(animation_group_name, sprite_sheet_index as u16);
-            let path = format!("./themes/{theme_name}/images/{animation_group_name}/{name}.png");
+    }
 
-            SpriteTexture
-            {
-                name,
-                texture_path: path.into(),
-                paletted: true,
-                filtered: false,
-            }
-        }).collect::<Vec<_>>()).collect::<Vec<_>>()
+    Err(anyhow!("Failed to get filename from: {}", path.to_str().unwrap_or("Failed to convert path to string")))
 }
 
-fn get_sprites(theme_config: &ThemeSerializationConfig) -> Vec<Sprite>
+fn get_theme_dirs(root: &PathBuf) -> Vec<PathBuf>
 {
-    theme_config.sprite_sheets.iter().flat_map(|sprite_sheet|
-    {
-        sprite_sheet.frame_mappings.iter().map(|frame_mapping|
-        {
-            let sprite_sheet_index = sprite_sheet.index;
-            let frame_index = frame_mapping.frame_index;
-            let animation_name = frame_mapping.animation_name.clone();
-
-            let name = make_sprite_name(&theme_config.group_name.clone(), frame_mapping.gif_index, frame_index);
-            let material_name = make_material_name(&theme_config.group_name, sprite_sheet_index as u16);
-
-            let total_width = sprite_sheet.width as f32;
-            let total_height = sprite_sheet.height as f32;
-            let x1 = frame_mapping.image_rect.x as f32;
-            let y1 = frame_mapping.image_rect.y as f32;
-            let x2 = x1 + frame_mapping.image_rect.width as f32;
-            let y2 = y1 + frame_mapping.image_rect.height as f32;
-            let color_map_coords = [x1 / total_width, y1 / total_height, x2 / total_width, y2 / total_height];
-
-            let palette_name = Some(format!("{}-default", animation_name).to_string()); //Some(format!("{theme_config.group_name}-palette-{frame_mapping.palette_variations_index}"));
-
-            Sprite
-            {
-                name, // {group_name}_{animation index}_{frame_index} ex: sub_1_1
-                material_name, // {group_name}_material_{sprite_sheet_index} ex: sub-sheet-0.indexed_material_0
-                color_map_coords, // [uv coords]
-                palette_name, // sub_side-default
-            }
-        }).collect::<Vec<_>>()
+    fs::read_dir(root).unwrap().filter_map(|e| e.ok()).filter_map(|e| {
+        let path = e.path();
+        if path.is_dir() {
+            Some(path)
+        } else {
+            None
+        }
     }).collect()
 }
 
-fn get_animations(theme_config: &ThemeSerializationConfig) -> Vec<SpriteAnimation>
+fn write_palette_to_file(filename: &PathBuf, palette: &[Srgba<u8>]) -> Result<()>
 {
-    let animation_offsets = theme_config.config.animations.iter().map(|anim|
-    {
-        (anim.animation.clone(), anim.offset)
-    }).collect::<FxHashMap<_, _>>();
-
-    let mut animations = FxHashMap::default();
-
-    theme_config.sprite_sheets.iter().for_each(|sprite_sheet|
+    let mut image = RgbaImage::new(palette.len() as u32, 1);
+    image.pixels_mut().zip(palette).par_bridge().for_each(|(pixel, original)|
     {
-        sprite_sheet.frame_mappings.iter().for_each(|frame_mapping|
-        {
-            let animation_group_name = theme_config.group_name.clone();
-            let animation_name = frame_mapping.animation_name.clone();
-            let color_map_coords = frame_mapping.uv_coords;
-            let duration = frame_mapping.duration;
-            let palette_name = Some(format!("{}-default", animation_name).to_string());
-            let sprite_name = make_sprite_name(&animation_group_name, frame_mapping.gif_index, frame_mapping.frame_index);
-
-            let animation_frame = SpriteAnimationFrame
-            {
-                color_map_coords,
-                duration,
-                palette_name,
-                sprite_name,
-            };
-
-            let frames = animations.entry(animation_name.clone()).or_insert_with(|| {
-                vec![]
-            });
-
-            frames.push(animation_frame);
-        });
+        *pixel = Rgba((*original).into());
     });
 
-    animations.into_iter().map(|(animation_name, frames)|
-    {
-        let offset = *animation_offsets.get(&animation_name).unwrap_or(&[0.0, 0.0]);
-        let palette_sets = theme_config.config.palette_sets.iter().filter(|p| p.animation == animation_name).map(|p|
-        {
-            PaletteSet
-            {
-                set_name: p.name.clone(),
-                frame_palettes: p.palettes.clone(),
-            }
-        }).collect::<Vec<_>>();
-
-        SpriteAnimation
-        {
-            name: animation_name.clone(),
-            offset,
-            looping: Some(true), // What is correct here
-            palette_sets,
-            frames,
-        }
-    }).collect::<Vec<_>>()
-}
-
-fn main() -> Result<()>
-{
-    // Inputs
-    let max_dimension = 1024;
-    let source_dir = PathBuf::from("./source_assets/");
-    let theme_dirs = get_theme_dirs(&source_dir);
-    //let theme_dirs = get_theme_gif_paths(&source_dir);
-    theme_dirs.par_iter().map(|theme| { process_theme(theme, &"./themes".into(), max_dimension) })
-        .inspect(log_error)
-        .flatten()
-        .for_each(|_| {});
-
+    image.save(filename)?;
     Ok(())
 }
+
diff --git a/src/theme_config_parser.rs b/src/theme_config_parser.rs
new file mode 100644
index 0000000000000000000000000000000000000000..08ce2e141776a4e7ff5f2c92aa1eda75271cb297
--- /dev/null
+++ b/src/theme_config_parser.rs
@@ -0,0 +1,91 @@
+use std::path::{Path, };
+use serde::{Deserialize, Serialize};
+use anyhow::{anyhow, Result};
+use fxhash::FxHashMap;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThemeGameEntityConfig
+{
+    pub game_entities: Vec<String>,
+    pub animation_states: Vec<String>,
+}
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThemeAnimationConfig
+{
+    pub source_animation: String,
+    pub offset: Option<[f32; 2]>,
+    pub game_entity_map: ThemeGameEntityConfig,
+    pub looping: Option<bool>,
+    pub max_colors: Option<u8>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThemeRequiredMappingConfig
+{
+    pub game_entity: String,
+    pub animation_states: Vec<String>,
+}
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThemeConfig
+{
+    pub name: String,
+    pub animations: Vec<ThemeAnimationConfig>,
+    pub required_mappings: Vec<ThemeRequiredMappingConfig>,
+}
+
+fn generate_entity_mappings(theme_config: &ThemeConfig) -> Result<FxHashMap<(String, String), String>>
+{
+    let mappings = theme_config.animations
+        .iter()
+        .flat_map(|anim_config| {
+            let entity_map = &anim_config.game_entity_map;
+            entity_map.game_entities.iter()
+                .flat_map(|game_entity| {
+                    entity_map.animation_states.iter().map(|anim_state|
+                    ((game_entity.clone(), anim_state.clone()), anim_config.source_animation.clone()))
+                })
+        })
+        .collect::<FxHashMap<_, _>>();
+
+    let errors = validate_mappings(&mappings, &theme_config.required_mappings);
+    if !errors.is_empty()
+    {
+        return Err(anyhow!("Failed to validate mappings: {:?}", errors));
+    }
+
+    Ok(mappings)
+}
+
+fn validate_mappings(theme_mappings: &FxHashMap<(String, String), String>, theme_requirements: &[ThemeRequiredMappingConfig]) -> Vec<String>
+{
+    let mut errors = Vec::new();
+
+    for mapping in theme_requirements
+    {
+        for animation_state in &mapping.animation_states
+        {
+            let game_entity = mapping.game_entity.clone();
+            let state = animation_state.clone();
+            let key = (game_entity.clone(), state.clone());
+            if !theme_mappings.contains_key(&key)
+            {
+                errors.push(format!("Missing mapping - entity: {}, state: {}", game_entity, state));
+            }
+        }
+    }
+
+    errors
+}
+
+pub fn load_theme_config(config_path: &Path) -> Result<ThemeConfig>
+{
+    let f = std::fs::File::open(config_path)?;
+    let config = serde_yaml::from_reader::<std::fs::File, ThemeConfig>(f).map(|c| c).map_err(|e| anyhow!(e))?;
+
+    if let Err(errors) = generate_entity_mappings(&config)
+    {
+        log::warn!("{}", errors);
+    }
+
+    Ok(config)
+}
diff --git a/src/theme_parser.rs b/src/theme_parser.rs
new file mode 100644
index 0000000000000000000000000000000000000000..18afd9568d9f587f6fed38f6dac2a33f76cdd058
--- /dev/null
+++ b/src/theme_parser.rs
@@ -0,0 +1,245 @@
+use std::fs::create_dir_all;
+use std::hash::{Hash, Hasher};
+use std::io::{Read, Write};
+use std::path::{PathBuf};
+use serde::{Deserialize, Serialize};
+use anyhow::{Error, Result};
+use fxhash::{FxHashMap, };
+use crate::{generate_sprite_sheets, log_error, render_color_sprite_sheet, write_palette_to_file};
+use crate::gif_processing::{PaletteColors, process_gif};
+use crate::theme_config_parser::ThemeConfig;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThemeAsset
+{
+    pub name: String,
+    pub path: String,
+    #[serde(rename = "type")]
+    pub type_name: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThemeSound
+{
+    pub entity: String,
+    pub entity_sound: String,
+    pub source: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
+pub struct ThemePaletteGroup
+{
+    pub name: String,
+    pub palettes: Vec<usize>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+pub struct ThemeAnimationFrame
+{
+    pub image: usize,
+    pub duration: f32,
+    pub width: u16,
+    pub height: u16,
+    pub uv_coords: [f32; 4],
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+pub struct ThemeAnimation
+{
+    pub name: String,
+    pub looping: bool,
+    pub offset: [f32; 2],
+    pub frames: Vec<ThemeAnimationFrame>,
+    pub palette_groups_index: Vec<usize>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+pub struct ThemeFile
+{
+    pub name: String,
+    pub image_paths: Vec<PathBuf>,
+    pub sound_paths: Vec<PathBuf>,
+    pub palette_paths: Vec<PathBuf>,
+
+    pub palette_groups: Vec<ThemePaletteGroup>,
+    pub animations: Vec<ThemeAnimation>,
+    pub game_to_theme_animation_map: FxHashMap<(String, String), usize>, // (game_entity, animation_state) -> animation index
+}
+
+impl ThemeFile
+{
+    pub fn load(file_path: &PathBuf) -> Result<Self>
+    {
+        let mut file = std::fs::File::open(file_path)?;
+        let mut bytes = Vec::new();
+        file.read_to_end(&mut bytes)?;
+        dbg!("Theme size: {}", bytes.len());
+        let result: Self = bincode::deserialize(&bytes)?;
+        Ok(result)
+    }
+}
+
+fn hash_palette(palette: &PaletteColors) -> u64
+{
+    let mut hasher = fxhash::FxHasher::default();
+    for color in palette.iter()
+    {
+        color.into_components().hash(&mut hasher);
+    }
+    hasher.finish()
+}
+
+pub fn serialize_theme_file(theme_file: &ThemeFile, file_path: &PathBuf) -> Result<()>
+{
+    let bin_theme_file_name = format!("{}.theme", theme_file.name);
+    let yaml_theme_file_name = format!("{}.yaml", theme_file.name);
+
+    let encoded: Vec<u8> = bincode::serialize(&theme_file)?;
+    let mut file = std::fs::File::create(file_path.join(&bin_theme_file_name))?;
+    file.write_all(&encoded)?;
+
+    let yaml = serde_yaml::to_string(&theme_file)?;
+    let mut file = std::fs::File::create(file_path.join(yaml_theme_file_name))?;
+    file.write_all(yaml.as_bytes())?;
+
+    Ok(())
+}
+
+pub fn generate_theme_file(theme_config: &ThemeConfig, theme_dir: &PathBuf, destination_dir: &PathBuf, max_dimensions: u16) -> Result<ThemeFile>
+{
+    let mut theme_file = ThemeFile {
+        name: theme_config.name.clone(),
+        image_paths: vec![], // Generated sprite sheets
+        sound_paths: vec![], // Copied directly from the theme
+        palette_paths: vec![], // Generated from animations
+        palette_groups: vec![], // Generated from animations
+        animations: vec![], // Generated from gif processing
+        game_to_theme_animation_map: FxHashMap::default(),
+    };
+
+    let source_animations_dir = theme_dir.join("animations");
+    let mut theme_gifs = Vec::new();
+    let mut color_palettes = FxHashMap::default();
+    let mut palette_groups_map = FxHashMap::default();
+
+    for anim in &theme_config.animations
+    {
+        //let theme_animation_frames = []; // Generated by loading the gif
+        let gif_path = source_animations_dir.join( format!("{}.gif", anim.source_animation));
+        let processed_gif = process_gif(&gif_path, anim.max_colors.unwrap_or(15))?;
+
+        let palettes_dir = destination_dir
+            .join(&theme_config.name)
+            .join("palettes");
+        create_dir_all(&palettes_dir)?;
+
+        // Each color group
+        let palette_groups_index = processed_gif.palette_variations.iter().enumerate()
+            .map(|(i, palette)|
+        {
+            let name = format!("{}_color_{i}", anim.source_animation);
+
+            // Each brightness palette
+            let palettes = palette.iter().map(|colors|
+            {
+                let hash_value = hash_palette(colors);
+                if let Some(palette_id) = color_palettes.get(&hash_value)
+                {
+                    Ok::<_, Error>(*palette_id)
+                }
+                else
+                {
+
+                    let name = format!("{hash_value}");
+                    let file_path = palettes_dir.join(format!("{}.palette.png", name));
+
+                    write_palette_to_file(&file_path, colors)?;
+
+                    theme_file.palette_paths.push(file_path.clone());
+                    let index = theme_file.palette_paths.len() - 1;
+                    color_palettes.insert(hash_value, index);
+
+                    Ok(index)
+                }
+            }).inspect(log_error)
+                .flatten()
+                .collect::<Vec<_>>();
+
+            if let Some(palette_group_index) = palette_groups_map.get(&name)
+            {
+                *palette_group_index
+            }
+            else
+            {
+                let index = theme_file.palette_groups.len();
+                let group = ThemePaletteGroup { palettes, name: name.clone() };
+                theme_file.palette_groups.push(group.clone());
+                palette_groups_map.insert(name.clone(), index);
+
+                index
+            }
+
+        }).collect::<Vec<_>>();
+
+        let sprite_sheet_destination = destination_dir
+            .join(&theme_config.name)
+            .join("sprite_sheets");
+
+        theme_gifs.push(processed_gif);
+        let gif_index = theme_gifs.len() - 1;
+
+        let sprite_sheets = generate_sprite_sheets(&sprite_sheet_destination,
+                                                   &anim.source_animation,
+                                                   max_dimensions,
+                                                   &theme_gifs[gif_index..=gif_index])?;
+
+        let render_destination = destination_dir
+            .join(&theme_config.name)
+            .join("renders");
+
+        let palette_variations = &theme_gifs[gif_index].palette_variations;
+        let sprite_sheet_image_index = sprite_sheets.iter().map(|sheet| {
+            render_color_sprite_sheet(&render_destination, sheet, palette_variations)?;
+            let image_index = theme_file.image_paths.len();
+            theme_file.image_paths.push(sheet.filename.clone());
+
+            Ok(image_index)
+        }).inspect(log_error).flatten().collect::<Vec<_>>();
+
+        let frames = sprite_sheets.iter()
+            .flat_map(|sprite_sheet| {
+                sprite_sheet.frame_mappings.iter().map(|frame| {
+                    ThemeAnimationFrame {
+                        image: sprite_sheet_image_index[frame.sprite_sheet_index as usize],
+                        width: frame.image_rect.width,
+                        height: frame.image_rect.height,
+                        duration: frame.duration,
+                        uv_coords: frame.uv_coords
+                    }
+                })
+            })
+            .collect();
+
+        let theme_animation = ThemeAnimation
+        {
+            name: anim.source_animation.clone(),
+            looping: anim.looping.unwrap_or(false),
+            offset: anim.offset.unwrap_or([0.0, 0.0]),
+            frames,
+            palette_groups_index // Generate from palettes that get generated during gif processing
+        };
+
+        theme_file.animations.push(theme_animation);
+        let anim_id = theme_file.animations.len() - 1;
+
+        for game_entity in &anim.game_entity_map.game_entities
+        {
+            for anim_state in &anim.game_entity_map.animation_states
+            {
+                theme_file.game_to_theme_animation_map.insert((game_entity.clone(), anim_state.clone()), anim_id);
+            }
+        }
+    }
+
+    Ok(theme_file)
+}
\ No newline at end of file