From 954c223d3276ae312274e9f0a3c47396b8cf9d60 Mon Sep 17 00:00:00 2001 From: Stanislaw Halik Date: Sat, 11 Jun 2022 10:56:22 +0200 Subject: . --- CMakeLists.txt | 2 + anim/CMakeLists.txt | 9 ++++ anim/atlas.cpp | 61 ++++++++++++++++++++++ anim/atlas.hpp | 36 +++++++++++++ anim/serialize.cpp | 129 +++++++++++++++++++++++++++++++++++++++++++++++ anim/serialize.hpp | 41 +++++++++++++++ crop-tool/CMakeLists.txt | 12 +++-- crop-tool/atlas.cpp | 61 ---------------------- crop-tool/atlas.hpp | 36 ------------- crop-tool/crop-tool.cpp | 58 ++++++++++----------- crop-tool/serialize.cpp | 127 ---------------------------------------------- crop-tool/serialize.hpp | 41 --------------- doc/atlas.json | 18 +++---- 13 files changed, 322 insertions(+), 309 deletions(-) create mode 100644 anim/CMakeLists.txt create mode 100644 anim/atlas.cpp create mode 100644 anim/atlas.hpp create mode 100644 anim/serialize.cpp create mode 100644 anim/serialize.hpp delete mode 100644 crop-tool/atlas.cpp delete mode 100644 crop-tool/atlas.hpp delete mode 100644 crop-tool/serialize.cpp delete mode 100644 crop-tool/serialize.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ac9065fb..d5245256 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,9 @@ if(NOT BOOTSTRAP_DEPENDS) find_package(MagnumPlugins QUIET REQUIRED) find_package(MagnumIntegration QUIET REQUIRED COMPONENTS Glm) + include_directories("${CMAKE_CURRENT_SOURCE_DIR}") add_subdirectory(crop-tool) + add_subdirectory(anim) corrade_add_resource(game_RESOURCES resources.conf) file(GLOB sources "*.cpp" CONFIGURE_ARGS) diff --git a/anim/CMakeLists.txt b/anim/CMakeLists.txt new file mode 100644 index 00000000..ae4bc542 --- /dev/null +++ b/anim/CMakeLists.txt @@ -0,0 +1,9 @@ +find_package(OpenCV QUIET REQUIRED COMPONENTS core imgcodecs imgproc) + +file(GLOB sources "*.cpp" CONFIGURE_ARGS) +set(self "${PROJECT_NAME}-anim") +add_library(${self} STATIC ${sources}) + +target_include_directories(${self} SYSTEM PRIVATE ${OpenCV_INCLUDE_DIRS}) +target_link_libraries(${self} opencv_imgproc opencv_imgcodecs opencv_core) +target_link_libraries(${self} Magnum::Magnum) diff --git a/anim/atlas.cpp b/anim/atlas.cpp new file mode 100644 index 00000000..ca05cd15 --- /dev/null +++ b/anim/atlas.cpp @@ -0,0 +1,61 @@ +#include "atlas.hpp" +#include "serialize.hpp" +#include "../defs.hpp" + +#include +#include + +void anim_atlas_row::add_entry(const anim_atlas_entry& x) +{ + auto& frame = *x.frame; + const auto& mat = x.mat; + frame.offset = {xpos, ypos}; + frame.size = {mat.cols, mat.rows}; + + ASSERT(mat.rows > 0 && mat.cols > 0); + data.push_back(x); + xpos += mat.cols; + max_height = std::max(mat.rows, max_height); +} + +void anim_atlas::advance_row() +{ + auto& row = rows.back(); + if (row.data.empty()) + return; + ASSERT(row.xpos); ASSERT(row.max_height); + ypos += row.max_height; + maxx = std::max(row.xpos, maxx); + rows.push_back({{}, 0, 0, ypos}); +} + +Magnum::Vector2i anim_atlas::offset() const +{ + const auto& row = rows.back(); + return {row.xpos, row.ypos}; +} + +Magnum::Vector2i anim_atlas::size() const +{ + const anim_atlas_row& row = rows.back(); + // prevent accidentally writing out of bounds by forgetting to call + // anim_atlas::advance_row() one last time prior to anim_atlas::size() + return {std::max(maxx, row.xpos), ypos + row.max_height}; +} + +bool anim_atlas::dump(const std::filesystem::path& filename) const +{ + auto sz = size(); + cv::Mat4b mat(sz[1], sz[0]); + mat.setTo(0); + + for (const anim_atlas_row& row : rows) + for (const anim_atlas_entry& x : row.data) + { + auto offset = x.frame->offset, size = x.frame->size; + cv::Rect roi = {offset[0], offset[1], size[0], size[1]}; + x.mat.copyTo(mat(roi)); + } + + return cv::imwrite(filename.string(), mat); +} diff --git a/anim/atlas.hpp b/anim/atlas.hpp new file mode 100644 index 00000000..dd6efabc --- /dev/null +++ b/anim/atlas.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include +#include + +struct anim_frame; + +namespace std::filesystem { class path; } + +struct anim_atlas_entry +{ + anim_frame* frame; + cv::Mat4b mat; +}; + +struct anim_atlas_row +{ + std::vector data; + int max_height = 0, xpos = 0, ypos = 0; + + void add_entry(const anim_atlas_entry& x); +}; + +class anim_atlas +{ + std::vector rows = {{}}; + int ypos = 0, maxx = 0; + +public: + void add_entry(const anim_atlas_entry& x) { rows.back().add_entry(x); } + void advance_row(); + Magnum::Vector2i offset() const; + Magnum::Vector2i size() const; + [[nodiscard]] bool dump(const std::filesystem::path& filename) const; +}; diff --git a/anim/serialize.cpp b/anim/serialize.cpp new file mode 100644 index 00000000..2a0b5a45 --- /dev/null +++ b/anim/serialize.cpp @@ -0,0 +1,129 @@ +#include "serialize.hpp" +#include "../json.hpp" + +#include +#include +#include +#include +#include + +using Corrade::Utility::Debug; +using Corrade::Utility::Error; + +static constexpr +std::pair anim_direction_map[] = { + { anim_direction::N, "n" }, + { anim_direction::NE, "ne" }, + { anim_direction::E, "e" }, + { anim_direction::SE, "se" }, + { anim_direction::S, "s" }, + { anim_direction::SW, "sw" }, + { anim_direction::W, "w" }, + { anim_direction::NW, "nw" }, +}; + +const char* anim_group::direction_to_string(anim_direction group) +{ + auto it = std::find_if(std::cbegin(anim_direction_map), std::cend(anim_direction_map), + [=](const auto& pair) { return group == pair.first; }); + if (it != std::cend(anim_direction_map)) + return it->second; + else + return "(unknown)"; +} + +anim_direction anim_group::string_to_direction(const std::string& str) +{ + auto it = std::find_if(std::cbegin(anim_direction_map), std::cend(anim_direction_map), + [&](const auto& pair) { return str == pair.second; }); + if (it != std::cend(anim_direction_map)) + return it->first; + else + return (anim_direction)0; +} + +namespace nlohmann { + +template<> +struct adl_serializer final { + static void to_json(json& j, anim_direction x); + static void from_json(const json& j, anim_direction& x); +}; + +template<> +struct adl_serializer final { + static void to_json(json& j, const Magnum::Vector2i& x); + static void from_json(const json& j, Magnum::Vector2i& x); +}; + +void adl_serializer::to_json(json& j, const Magnum::Vector2i& x) +{ + j["x"] = x[0]; + j["y"] = x[1]; +} + +void adl_serializer::from_json(const json& j, Magnum::Vector2i& x) +{ + j.at("x").get_to(x[0]); + j.at("y").get_to(x[1]); +} + +#if 0 +void adl_serializer::to_json(json& j, anim_direction x) +{ + j = anim_group::direction_to_string(x); +} + +void adl_serializer::from_json(const json& j, anim_direction& x) +{ + x = anim_group::string_to_direction(j); +} +#endif + +} // namespace nlohmann + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_frame, ground, offset, size); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_group, name, frames, ground); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim, name, nframes, actionframe, fps, groups); + +std::optional anim::from_json(const std::filesystem::path& pathname) +{ + using namespace nlohmann; + std::ifstream s; + s.exceptions(s.exceptions() | std::ios::failbit | std::ios::badbit); + try { + s.open(pathname, std::ios_base::in); + } catch (const std::ios::failure& e) { + Error{} << "failed to open" << pathname << ':' << e.what(); + return std::nullopt; + } + anim ret; + try { + json j; + s >> j; + using nlohmann::from_json; + from_json(j, ret); + } catch (const std::exception& e) { + Error{} << "failed to parse" << pathname << ':' << e.what(); + return std::nullopt; + } + return std::make_optional(std::move(ret)); +} + +bool anim::to_json(const std::filesystem::path& pathname) +{ + nlohmann::json j = *this; + + std::ofstream s; + s.exceptions(s.exceptions() | std::ios::failbit | std::ios::badbit); + try { + s.open(pathname, std::ios_base::out | std::ios_base::trunc); + } catch (const std::ios::failure& e) { + Error{} << "failed to open" << pathname << "for writing:" << e.what(); + return false; + } + s << j.dump(4); + s.flush(); + + return true; +} diff --git a/anim/serialize.hpp b/anim/serialize.hpp new file mode 100644 index 00000000..10fd9e18 --- /dev/null +++ b/anim/serialize.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "../defs.hpp" +#include +#include +#include +#include +#include +#include + +struct anim_frame final +{ + Magnum::Vector2i ground, offset, size; +}; + +enum class anim_direction : unsigned char +{ + N, NE, E, SE, S, SW, W, NW, + COUNT = NW + 1, +}; + +struct anim_group final +{ + std::string name; + std::vector frames; + Magnum::Vector2i ground; + + static const char* direction_to_string(anim_direction group); + static anim_direction string_to_direction(const std::string& str); +}; + +struct anim final +{ + static std::optional from_json(const std::filesystem::path& pathname); + bool to_json(const std::filesystem::path& pathname); + static constexpr int default_fps = 24; + + std::string name; + std::array groups; + int nframes = 0, actionframe = -1, fps = default_fps; +}; diff --git a/crop-tool/CMakeLists.txt b/crop-tool/CMakeLists.txt index cc38c51b..974a07e9 100644 --- a/crop-tool/CMakeLists.txt +++ b/crop-tool/CMakeLists.txt @@ -1,6 +1,8 @@ -find_package(OpenCV QUIET REQUIRED COMPONENTS core imgcodecs imgproc highgui) +find_package(OpenCV QUIET REQUIRED COMPONENTS core imgcodecs imgproc) +set(self "${PROJECT_NAME}-crop-tool") file(GLOB sources "*.cpp" CONFIGURE_ARGS) -add_executable(crop-tool ${sources}) -target_include_directories(crop-tool SYSTEM PRIVATE ${OpenCV_INCLUDE_DIRS}) -target_link_libraries(crop-tool opencv_highgui opencv_imgproc opencv_imgcodecs opencv_core) -target_link_libraries(crop-tool Corrade::Utility Magnum::Trade) + +add_executable(${self} ${sources}) +target_include_directories(${self} SYSTEM PRIVATE ${OpenCV_INCLUDE_DIRS}) +target_link_libraries(${self} Corrade::Utility) +target_link_libraries(${self} game-anim) diff --git a/crop-tool/atlas.cpp b/crop-tool/atlas.cpp deleted file mode 100644 index ca05cd15..00000000 --- a/crop-tool/atlas.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "atlas.hpp" -#include "serialize.hpp" -#include "../defs.hpp" - -#include -#include - -void anim_atlas_row::add_entry(const anim_atlas_entry& x) -{ - auto& frame = *x.frame; - const auto& mat = x.mat; - frame.offset = {xpos, ypos}; - frame.size = {mat.cols, mat.rows}; - - ASSERT(mat.rows > 0 && mat.cols > 0); - data.push_back(x); - xpos += mat.cols; - max_height = std::max(mat.rows, max_height); -} - -void anim_atlas::advance_row() -{ - auto& row = rows.back(); - if (row.data.empty()) - return; - ASSERT(row.xpos); ASSERT(row.max_height); - ypos += row.max_height; - maxx = std::max(row.xpos, maxx); - rows.push_back({{}, 0, 0, ypos}); -} - -Magnum::Vector2i anim_atlas::offset() const -{ - const auto& row = rows.back(); - return {row.xpos, row.ypos}; -} - -Magnum::Vector2i anim_atlas::size() const -{ - const anim_atlas_row& row = rows.back(); - // prevent accidentally writing out of bounds by forgetting to call - // anim_atlas::advance_row() one last time prior to anim_atlas::size() - return {std::max(maxx, row.xpos), ypos + row.max_height}; -} - -bool anim_atlas::dump(const std::filesystem::path& filename) const -{ - auto sz = size(); - cv::Mat4b mat(sz[1], sz[0]); - mat.setTo(0); - - for (const anim_atlas_row& row : rows) - for (const anim_atlas_entry& x : row.data) - { - auto offset = x.frame->offset, size = x.frame->size; - cv::Rect roi = {offset[0], offset[1], size[0], size[1]}; - x.mat.copyTo(mat(roi)); - } - - return cv::imwrite(filename.string(), mat); -} diff --git a/crop-tool/atlas.hpp b/crop-tool/atlas.hpp deleted file mode 100644 index 675b6ff9..00000000 --- a/crop-tool/atlas.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include -#include -#include -#include - -struct anim_frame; - -namespace std::filesystem { class path; } - -struct anim_atlas_entry -{ - anim_frame* frame; - const cv::Mat4b mat; -}; - -struct anim_atlas_row -{ - std::vector data; - int max_height = 0, xpos = 0, ypos = 0; - - void add_entry(const anim_atlas_entry& x); -}; - -class anim_atlas -{ - std::vector rows = {{}}; - int ypos = 0, maxx = 0; - -public: - void add_entry(const anim_atlas_entry& x) { rows.back().add_entry(x); } - void advance_row(); - Magnum::Vector2i offset() const; - Magnum::Vector2i size() const; - [[nodiscard]] bool dump(const std::filesystem::path& filename) const; -}; diff --git a/crop-tool/crop-tool.cpp b/crop-tool/crop-tool.cpp index 35089dd6..099ebfa1 100644 --- a/crop-tool/crop-tool.cpp +++ b/crop-tool/crop-tool.cpp @@ -1,13 +1,12 @@ #include "../defs.hpp" -#include "atlas.hpp" -#include "serialize.hpp" +#include "anim/atlas.hpp" +#include "anim/serialize.hpp" #include #include #include #include #include #include -#include #include #include #include @@ -35,9 +34,10 @@ using Corrade::Utility::Debug; using std::filesystem::path; -struct options_ { - std::optional width, height; - std::optional scale; +struct options +{ + unsigned width = 0, height = 0; + double scale = 0; path input_dir, output_dir; }; @@ -71,7 +71,7 @@ static std::tuple find_image_bounds(const path& path } [[nodiscard]] -static bool load_file(anim_group& group, options_& opts, anim_atlas& atlas, const path& filename) +static bool load_file(anim_group& group, options& opts, anim_atlas& atlas, const path& filename) { auto mat = progn( cv::Mat mat_ = cv::imread(filename.string(), cv::IMREAD_UNCHANGED); @@ -93,18 +93,18 @@ static bool load_file(anim_group& group, options_& opts, anim_atlas& atlas, cons cv::Size size{end - start}, dest_size; - if (!opts.scale) + if (opts.scale == 0.0) { + ASSERT(opts.width || opts.height); if (opts.width) - opts.scale = (double)*opts.width / size.width; - else if (opts.height) - opts.scale = (double)*opts.height / size.height; + opts.scale = (double)opts.width / size.width; else - std::abort(); + opts.scale = (double)opts.height / size.height; + ASSERT(opts.scale > 1e-6); } - dest_size = {(int)std::round(*opts.scale * size.width), - (int)std::round(*opts.scale * size.height)}; + dest_size = {(int)std::round(opts.scale * size.width), + (int)std::round(opts.scale * size.height)}; if (size.width < dest_size.width || size.height < dest_size.height) { @@ -122,19 +122,17 @@ static bool load_file(anim_group& group, options_& opts, anim_atlas& atlas, cons } #endif Magnum::Vector2i ground = { - (int)std::round((group.ground[0] - start[0]) * *opts.scale), - (int)std::round((group.ground[1] - start[1]) * *opts.scale), + (int)std::round((group.ground[0] - start[0]) * opts.scale), + (int)std::round((group.ground[1] - start[1]) * opts.scale), }; - auto offset = atlas.offset(); - - group.frames.push_back({ground, offset, {dest_size.width, dest_size.height}}); + group.frames.push_back({ground, atlas.offset(), {dest_size.width, dest_size.height}}); atlas.add_entry({&group.frames.back(), std::move(mat)}); return true; } [[nodiscard]] -static bool load_directory(anim_group& group, options_& opts, anim_atlas& atlas, const path& input_dir) +static bool load_directory(anim_group& group, options& opts, anim_atlas& atlas, const path& input_dir) { if (std::error_code ec{}; !std::filesystem::exists(input_dir/".", ec)) { @@ -184,7 +182,7 @@ static char* fix_argv0(char* argv0) return argv0; } -static std::tuple parse_cmdline(int argc, const char* const* argv) +static std::tuple parse_cmdline(int argc, const char* const* argv) { Corrade::Utility::Arguments args{}; args.addOption('o', "output") @@ -192,23 +190,23 @@ static std::tuple parse_cmdline(int argc, const char* const* argv .addOption('W', "width", "") .addOption('H', "height", ""); args.parse(argc, argv); - options_ options; + options opts; if (unsigned w = args.value("width"); w != 0) - options.width = w; + opts.width = w; if (unsigned h = args.value("height"); h != 0) - options.height = h; - if (!(!options.width ^ !options.height)) + opts.height = h; + if (!(!opts.width ^ !opts.height)) { Error{} << "exactly one of --width, --height must be given"; goto usage; } - options.output_dir = args.value("output"); - options.input_dir = args.value("directory"); + opts.output_dir = args.value("output"); + opts.input_dir = args.value("directory"); - if (options.output_dir.empty()) - options.output_dir = options.input_dir; + if (opts.output_dir.empty()) + opts.output_dir = opts.input_dir; - return {options, 0}; + return { opts, 0}; usage: Error{Error::Flag::NoNewlineAtTheEnd} << Corrade::Containers::StringView{args.usage()}; return {{}, EX_USAGE}; diff --git a/crop-tool/serialize.cpp b/crop-tool/serialize.cpp deleted file mode 100644 index acefe436..00000000 --- a/crop-tool/serialize.cpp +++ /dev/null @@ -1,127 +0,0 @@ -#include "serialize.hpp" -#include "../json.hpp" - -#include -#include -#include -#include -#include - -using Corrade::Utility::Debug; -using Corrade::Utility::Error; - -static constexpr -std::pair anim_direction_map[] = { - { anim_direction::N, "n" }, - { anim_direction::NE, "ne" }, - { anim_direction::E, "e" }, - { anim_direction::SE, "se" }, - { anim_direction::S, "s" }, - { anim_direction::SW, "sw" }, - { anim_direction::W, "w" }, - { anim_direction::NW, "nw" }, -}; - -const char* anim_group::direction_to_string(anim_direction group) -{ - auto it = std::find_if(std::cbegin(anim_direction_map), std::cend(anim_direction_map), - [=](const auto& pair) { return group == pair.first; }); - if (it != std::cend(anim_direction_map)) - return it->second; - else - return "(unknown)"; -} - -anim_direction anim_group::string_to_direction(const std::string& str) -{ - auto it = std::find_if(std::cbegin(anim_direction_map), std::cend(anim_direction_map), - [&](const auto& pair) { return str == pair.second; }); - if (it != std::cend(anim_direction_map)) - return it->first; - else - return (anim_direction)0; -} - -namespace nlohmann { - -template<> -struct adl_serializer final { - static void to_json(json& j, anim_direction x); - static void from_json(const json& j, anim_direction& x); -}; - -template<> -struct adl_serializer final { - static void to_json(json& j, const Magnum::Vector2i& x); - static void from_json(const json& j, Magnum::Vector2i& x); -}; - -void adl_serializer::to_json(json& j, const Magnum::Vector2i& x) -{ - j["x"] = x[0]; - j["y"] = x[1]; -} - -void adl_serializer::from_json(const json& j, Magnum::Vector2i& x) -{ - j.at("x").get_to(x[0]); - j.at("y").get_to(x[1]); -} - -void adl_serializer::to_json(json& j, anim_direction x) -{ - j = anim_group::direction_to_string(x); -} - -void adl_serializer::from_json(const json& j, anim_direction& x) -{ - x = anim_group::string_to_direction(j); -} - -} // namespace nlohmann - -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_frame, ground, offset, size); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_group, name, frames, ground); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim, name, nframes, actionframe, fps, groups); - -std::optional anim::from_json(const std::filesystem::path& pathname) -{ - using namespace nlohmann; - std::ifstream s; - s.exceptions(s.exceptions() | std::ios::failbit | std::ios::badbit); - try { - s.open(pathname, std::ios_base::in); - } catch (const std::ios::failure& e) { - Error{} << "failed to open" << pathname << ':' << e.what(); - return std::nullopt; - } - anim ret; - try { - json j; - s >> j; - using nlohmann::from_json; - from_json(j, ret); - } catch (const std::exception& e) { - Error{} << "failed to parse" << pathname << ':' << e.what(); - return std::nullopt; - } - return std::make_optional(std::move(ret)); -} - -bool anim::to_json(const std::filesystem::path& pathname) -{ - nlohmann::json j = *this; - - std::ofstream s; - s.exceptions(s.exceptions() | std::ios::failbit | std::ios::badbit); - try { - s.open(pathname, std::ios_base::out | std::ios_base::trunc); - } catch (const std::ios::failure& e) { - Error{} << "failed to open" << pathname << "for writing:" << e.what(); - return false; - } - s << j.dump(4); - s.flush(); - - return true; -} diff --git a/crop-tool/serialize.hpp b/crop-tool/serialize.hpp deleted file mode 100644 index 10fd9e18..00000000 --- a/crop-tool/serialize.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "../defs.hpp" -#include -#include -#include -#include -#include -#include - -struct anim_frame final -{ - Magnum::Vector2i ground, offset, size; -}; - -enum class anim_direction : unsigned char -{ - N, NE, E, SE, S, SW, W, NW, - COUNT = NW + 1, -}; - -struct anim_group final -{ - std::string name; - std::vector frames; - Magnum::Vector2i ground; - - static const char* direction_to_string(anim_direction group); - static anim_direction string_to_direction(const std::string& str); -}; - -struct anim final -{ - static std::optional from_json(const std::filesystem::path& pathname); - bool to_json(const std::filesystem::path& pathname); - static constexpr int default_fps = 24; - - std::string name; - std::array groups; - int nframes = 0, actionframe = -1, fps = default_fps; -}; diff --git a/doc/atlas.json b/doc/atlas.json index 53f9722c..c5b05b15 100644 --- a/doc/atlas.json +++ b/doc/atlas.json @@ -2,16 +2,16 @@ "name": "character", "nframes": 24, "actionframe": 0, - "fps": 1, + "fps": 24, "groups": [ - { "direction": "N", "frames": [], "ground": {"x":484, "y":488} }, - { "direction": "NE", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "E", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "SE", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "S", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "SW", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "W", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "NW", "frames": [], "ground": {"x": 0, "y": 0} } + { "name": "n", "frames": [], "ground": {"x":484, "y":488} }, + { "name": "ne", "frames": [], "ground": {"x": 0, "y": 0} }, + { "name": "e", "frames": [], "ground": {"x": 0, "y": 0} }, + { "name": "se", "frames": [], "ground": {"x": 0, "y": 0} }, + { "name": "s", "frames": [], "ground": {"x": 0, "y": 0} }, + { "name": "sw", "frames": [], "ground": {"x": 0, "y": 0} }, + { "name": "w", "frames": [], "ground": {"x": 0, "y": 0} }, + { "name": "nw", "frames": [], "ground": {"x": 0, "y": 0} } ] } -- cgit v1.2.3