Refactor...

This commit is contained in:
2025-10-21 20:23:27 -04:00
parent 7f07eaa128
commit 5b0f9a39c4
104 changed files with 17010 additions and 13171 deletions

View File

@@ -1,4 +1,11 @@
BasedOnStyle: LLVM
ColumnLimit: 160
ColumnLimit: 120
PointerAlignment: Left
ReferenceAlignment: Left
ReferenceAlignment: Left
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: true
CommentPragmas: '^'
BreakBeforeBraces: Allman
NamespaceIndentation: All
FixNamespaceComments: false
IndentCaseLabels: true
IndentPPDirectives: BeforeHash

5
.gitignore vendored
View File

@@ -5,8 +5,7 @@ packed/
vcpkg_installed/
out/
external/
external/
external/
external/
workshop/resources
cmake-build-debug/
.vs/
.idea/

3
.gitmodules vendored
View File

@@ -11,3 +11,6 @@
[submodule "external/SDL"]
path = external/SDL
url = https://github.com/libsdl-org/SDL.git
[submodule "external/lunasvg"]
path = external/lunasvg
url = https://github.com/sammycage/lunasvg

28
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug ANM2Ed",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/anm2ed",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing"
},
{
"description": "Set disassembly flavor to Intel",
"text": "-gdb-set disassembly-flavor intel"
}
]
}
]
}

17
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cmake --build build --target anm2ed",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
}
]
}

View File

@@ -1,90 +1,103 @@
cmake_minimum_required(VERSION 3.15)
cmake_minimum_required(VERSION 3.30)
project(anm2ed CXX)
# Optional: auto-pick up vcpkg toolchain on Windows
if(WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
if (WIN32 AND DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
CACHE STRING "Vcpkg toolchain file")
endif()
CACHE STRING "Vcpkg toolchain file")
endif ()
find_package(OpenGL REQUIRED)
# Export compile_commands.json (for clangd, etc.)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(CMAKE_EXPORT_COMPILE_COMMANDS)
if (CMAKE_EXPORT_COMPILE_COMMANDS)
execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
${CMAKE_BINARY_DIR}/compile_commands.json
${CMAKE_SOURCE_DIR}/compile_commands.json
${CMAKE_BINARY_DIR}/compile_commands.json
${CMAKE_SOURCE_DIR}/compile_commands.json
)
endif()
endif ()
set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp)
set(IMGUI_SRC
external/imgui/imgui.cpp
external/imgui/imgui_draw.cpp
external/imgui/imgui_widgets.cpp
external/imgui/imgui_tables.cpp
external/imgui/backends/imgui_impl_sdl3.cpp
external/imgui/backends/imgui_impl_opengl3.cpp
)
set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
src/*.cpp
src/*.h
)
add_executable(${PROJECT_NAME}
${GLAD_SRC}
${IMGUI_SRC}
${TINYXML2_SRC}
${PROJECT_SRC}
)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
add_subdirectory(external/SDL EXCLUDE_FROM_ALL)
if(WIN32)
add_subdirectory(external/lunasvg)
set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp)
set(IMGUI_SRC
external/imgui/imgui.cpp
external/imgui/imgui_draw.cpp
external/imgui/imgui_widgets.cpp
external/imgui/imgui_tables.cpp
external/imgui/backends/imgui_impl_sdl3.cpp
external/imgui/backends/imgui_impl_opengl3.cpp
)
set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
src/*.cpp
src/*.h
)
add_executable(${PROJECT_NAME}
${GLAD_SRC}
${IMGUI_SRC}
${TINYXML2_SRC}
${PROJECT_SRC}
)
if (WIN32)
enable_language(RC)
target_sources(${PROJECT_NAME} PRIVATE Icon.rc)
set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE)
set_property(TARGET ${PROJECT_NAME} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
target_compile_options(${PROJECT_NAME} PRIVATE /EHsc)
target_link_options(${PROJECT_NAME} PRIVATE /STACK:0xffffff)
else()
else ()
target_compile_options(${PROJECT_NAME} PRIVATE
-O2 -Wall -Wextra -pedantic -fmax-errors=1
-O2 -Wall -Wextra -pedantic
)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG)
target_compile_options(${PROJECT_NAME} PRIVATE -g)
endif()
target_compile_options(${PROJECT_NAME} PRIVATE -pg)
else ()
set(CMAKE_BUILD_TYPE "Release")
endif ()
target_link_libraries(${PROJECT_NAME} PRIVATE m)
endif()
endif ()
target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_OBSOLETE_FUNCTIONS IMGUI_DEBUG_PARANOID IMGUI_ENABLE_DOCKING)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
target_include_directories(${PROJECT_NAME} PRIVATE
external
external/imgui
external/glm
external/tinyxml2
include
include/glad
src
target_compile_definitions(${PROJECT_NAME} PRIVATE
IMGUI_DISABLE_OBSOLETE_FUNCTIONS
IMGUI_DEBUG_PARANOID
IMGUI_ENABLE_DOCKING
)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL SDL3::SDL3-static)
target_include_directories(${PROJECT_NAME} PRIVATE
external
external/imgui
external/glm
external/tinyxml2
external/lunasvg
include
include/glad
src
src/imgui
src/resource
src/util
)
target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static lunasvg)
message(STATUS "System: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Project: ${PROJECT_NAME}")
message(STATUS "Build: ${CMAKE_BUILD_TYPE}")
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}")
message(STATUS "Build: ${CMAKE_BUILD_TYPE}")

View File

@@ -1,26 +0,0 @@
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\build\\${name}",
"installRoot": "${projectDir}\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
},
{
"name": "x64-Release",
"generator": "Ninja",
"configurationType": "Release",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\build\\${name}",
"installRoot": "${projectDir}\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1 +0,0 @@
IDI_ICON1 ICON DISCARDABLE "Icon.ico"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
INPUT="atlas.png"
OUTPUT="../src/PACKED.h"
TMP="atlas.bytes"
# Ensure deps
command -v optipng >/dev/null 2>&1 || { echo "error: optipng not found in PATH" >&2; exit 1; }
command -v xxd >/dev/null 2>&1 || { echo "error: xxd not found in PATH" >&2; exit 1; }
# 1) Optimize PNG in place
optipng -o7 "$INPUT" >/dev/null
# 2) Extract ONLY the bytes from xxd -i (between '= {' and '};')
# Using awk avoids the earlier '{' vs '= {' mismatch bug.
xxd -i "$INPUT" \
| awk '
/= *\{/ {inside=1; next}
inside && /};/ {inside=0; exit}
inside {print}
' > "$TMP"
# Sanity check: make sure we got something
if ! [ -s "$TMP" ]; then
echo "error: failed to extract bytes from xxd output" >&2
exit 1
fi
# 3) Replace ONLY the bytes inside TEXTURE_ATLAS[] initializer
# - Find the exact declaration line for TEXTURE_ATLAS
# - Print that line
# - On the following line with just '{', print it, then insert bytes,
# then skip everything until the matching '};' and print that once.
awk -v tmpfile="$TMP" '
BEGIN { state=0 } # 0=normal, 1=after decl waiting for {, 2=skipping old bytes until };
# Match the TEXTURE_ATLAS declaration line precisely
$0 ~ /^[[:space:]]*const[[:space:]]+u8[[:space:]]+TEXTURE_ATLAS\[\][[:space:]]*=/ {
print; state=1; next
}
# After the decl, the next line with a lone "{" starts the initializer
state==1 && $0 ~ /^[[:space:]]*{[[:space:]]*$/ {
print # print the opening brace line
while ((getline line < tmpfile) > 0) print line # insert fresh bytes
close(tmpfile)
state=2 # now skip old initializer content until we hit the closing "};"
next
}
# While skipping, suppress lines until the closing "};", which we reprint once
state==2 {
if ($0 ~ /^[[:space:]]*};[[:space:]]*$/) {
print # print the closing brace+semicolon
state=0
}
next
}
# Default: pass through unchanged
{ print }
' "$OUTPUT" > "$OUTPUT.tmp" && mv "$OUTPUT.tmp" "$OUTPUT"
rm -f "$TMP"
echo "Updated $OUTPUT with optimized bytes from $INPUT."

2
external/imgui vendored

3121
include/nanosvg.h Normal file

File diff suppressed because it is too large Load Diff

1461
include/nanosvgrast.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,400 +0,0 @@
#pragma once
#include <set>
#define GLAD_GL_IMPLEMENTATION
#include <SDL3/SDL.h>
#include <glad/glad.h>
#include <glm/glm/glm.hpp>
#include <glm/glm/gtc/matrix_transform.hpp>
#include <glm/glm/gtc/type_ptr.hpp>
#include <tinyxml2.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <format>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <print>
#include <ranges>
#include <string>
#include <type_traits>
#include <unordered_set>
#include <variant>
#include <vector>
using namespace glm;
#define PREFERENCES_DIRECTORY "anm2ed"
#define ROUND_NEAREST_MULTIPLE(value, multiple) (roundf((value) / (multiple)) * (multiple))
#define FLOAT_TO_UINT8(x) (static_cast<uint8_t>((x) * 255.0f))
#define UINT8_TO_FLOAT(x) ((x) / 255.0f)
#define PERCENT_TO_UNIT(x) (x / 100.0f)
#define UNIT_TO_PERCENT(x) (x * 100.0f)
#define SECOND 1000.0f
#define TICK_DELAY (SECOND / 30.0)
#define UPDATE_DELAY (SECOND / 120.0)
#define ID_NONE -1
#define INDEX_NONE -1
#define VALUE_NONE -1
#define TIME_NONE -1.0f
#define GL_ID_NONE 0
#ifdef _WIN32
#define POPEN _popen
#define PCLOSE _pclose
#define PWRITE_MODE "wb"
#define PREAD_MODE "r"
#else
#define POPEN popen
#define PCLOSE pclose
#define PWRITE_MODE "w"
#define PREAD_MODE "r"
#endif
static const GLuint GL_TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
static const vec4 COLOR_RED = {1.0f, 0.0f, 0.0f, 1.0f};
static const vec4 COLOR_GREEN = {0.0f, 1.0f, 0.0f, 1.0f};
static const vec4 COLOR_BLUE = {0.0f, 0.0f, 1.0f, 1.0f};
static const vec4 COLOR_PINK = {1.0f, 0.0f, 1.0f, 1.0f};
static const vec4 COLOR_OPAQUE = {1.0f, 1.0f, 1.0f, 1.0f};
static const vec4 COLOR_TRANSPARENT = {0.0f, 0.0f, 0.0f, 0.0f};
static const vec3 COLOR_OFFSET_NONE = {0.0f, 0.0f, 0.0f};
static inline std::string preferences_path_get(void) {
char* preferencesPath = SDL_GetPrefPath("", PREFERENCES_DIRECTORY);
std::string preferencesPathString = preferencesPath;
SDL_free(preferencesPath);
return preferencesPathString;
}
static inline bool string_to_bool(const std::string& string) {
if (string == "1")
return true;
std::string lower = string;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return lower == "true";
}
static inline std::string string_quote(const std::string& string) { return "\"" + string + "\""; }
static inline std::string string_to_lowercase(std::string string) {
std::transform(string.begin(), string.end(), string.begin(), [](char character) { return std::tolower(character); });
return string;
}
static inline std::string string_backslash_replace(std::string string) {
for (char& character : string)
if (character == '\\')
character = '/';
return string;
}
#define FLOAT_FORMAT_MAX_DECIMALS 5
#define FLOAT_FORMAT_EPSILON 1e-5f
static constexpr float FLOAT_FORMAT_POW10[] = {1.f, 10.f, 100.f, 1000.f, 10000.f, 100000.f};
static inline int float_decimals_needed(float value) {
for (int decimalCount = 0; decimalCount <= FLOAT_FORMAT_MAX_DECIMALS; ++decimalCount) {
float scale = FLOAT_FORMAT_POW10[decimalCount];
float rounded = roundf(value * scale) / scale;
if (fabsf(value - rounded) < FLOAT_FORMAT_EPSILON)
return decimalCount;
}
return FLOAT_FORMAT_MAX_DECIMALS;
}
static inline const char* float_format_get(float value) {
static std::string formatString;
const int decimalCount = float_decimals_needed(value);
formatString = (decimalCount == 0) ? "%.0f" : ("%." + std::to_string(decimalCount) + "f");
return formatString.c_str();
}
static inline const char* vec2_format_get(const vec2& value) {
static std::string formatString;
const int decimalCountX = float_decimals_needed(value.x);
const int decimalCountY = float_decimals_needed(value.y);
const int decimalCount = (decimalCountX > decimalCountY) ? decimalCountX : decimalCountY;
formatString = (decimalCount == 0) ? "%.0f" : ("%." + std::to_string(decimalCount) + "f");
return formatString.c_str();
}
static inline std::string working_directory_from_file_set(const std::string& path) {
std::filesystem::path filePath = path;
std::filesystem::path parentPath = filePath.parent_path();
std::filesystem::current_path(parentPath);
return parentPath.string();
};
static inline std::string path_extension_change(const std::string& path, const std::string& extension) {
std::filesystem::path filePath(path);
filePath.replace_extension(extension);
return filePath.string();
}
static inline bool path_is_extension(const std::string& path, const std::string& extension) {
auto e = std::filesystem::path(path).extension().string();
std::transform(e.begin(), e.end(), e.begin(), ::tolower);
return e == ("." + extension);
}
static inline bool path_exists(const std::filesystem::path& pathCheck) {
std::error_code errorCode;
return std::filesystem::exists(pathCheck, errorCode) && ((void)std::filesystem::status(pathCheck, errorCode), !errorCode);
}
static inline bool path_is_valid(const std::filesystem::path& pathCheck) {
namespace fs = std::filesystem;
std::error_code ec;
if (fs::is_directory(pathCheck, ec))
return false;
fs::path parentDir = pathCheck.has_parent_path() ? pathCheck.parent_path() : fs::path(".");
if (!fs::is_directory(parentDir, ec))
return false;
bool existedBefore = fs::exists(pathCheck, ec);
std::ofstream testStream(pathCheck, std::ios::app | std::ios::binary);
bool isValid = testStream.is_open();
testStream.close();
if (!existedBefore && isValid)
fs::remove(pathCheck, ec);
return isValid;
}
static inline int string_to_enum(const std::string& string, const char* const* array, int n) {
for (int i = 0; i < n; i++)
if (string == array[i])
return i;
return -1;
};
template <typename T> T& dummy_value() {
static T value{};
return value;
}
template <typename T> static inline int map_next_id_get(const std::map<int, T>& map) {
int id = 0;
for (const auto& [key, value] : map) {
if (key != id)
break;
++id;
}
return id;
}
template <typename Map> static inline auto map_find(Map& map, typename Map::key_type id) -> typename Map::mapped_type* {
if (auto it = map.find(id); it != map.end())
return &it->second;
return nullptr;
}
template <typename Map, typename Key> static inline void map_swap(Map& map, const Key& key1, const Key& key2) {
if (key1 == key2)
return;
auto it1 = map.find(key1);
auto it2 = map.find(key2);
if (it1 != map.end() && it2 != map.end()) {
using std::swap;
swap(it1->second, it2->second);
} else if (it1 != map.end()) {
map[key2] = std::move(it1->second);
map.erase(it1);
} else if (it2 != map.end()) {
map[key1] = std::move(it2->second);
map.erase(it2);
}
};
template <typename T> static inline void map_insert_shift(std::map<int, T>& map, int id, const T& value) {
const int insertID = id + 1;
std::vector<std::pair<int, T>> toShift;
for (auto it = map.rbegin(); it != map.rend(); ++it) {
if (it->first < insertID)
break;
toShift.emplace_back(it->first + 1, std::move(it->second));
}
for (const auto& [newKey, _] : toShift)
map.erase(newKey - 1);
for (auto& [newKey, val] : toShift)
map[newKey] = std::move(val);
map[insertID] = value;
}
template <typename Map> auto map_keys_to_set(const Map& m) {
using Key = typename Map::key_type;
std::unordered_set<Key> s;
s.reserve(m.size());
for (const auto& [key, _] : m) {
s.insert(key);
}
return s;
}
template <typename Set> Set set_symmetric_difference(const Set& a, const Set& b) {
Set result;
result.reserve(a.size() + b.size());
for (const auto& x : a) {
if (!b.contains(x)) {
result.insert(x);
}
}
for (const auto& x : b) {
if (!a.contains(x)) {
result.insert(x);
}
}
return result;
}
template <typename T> static inline T* vector_find(std::vector<T>& v, const T& value) {
auto it = std::find(v.begin(), v.end(), value);
return (it != v.end()) ? &(*it) : nullptr;
}
template <typename T> static inline void vector_value_erase(std::vector<T>& v, const T& value) { v.erase(std::remove(v.begin(), v.end(), value), v.end()); }
template <typename T> static inline void vector_value_swap(std::vector<T>& v, const T& a, const T& b) {
for (auto& element : v) {
if (element == a)
element = b;
else if (element == b)
element = a;
}
}
template <typename T> static inline T* vector_get(std::vector<T>& v, size_t index) {
if (index < v.size())
return &v[index];
return nullptr;
}
template <typename T> static inline void vector_move(std::vector<T>& v, size_t from, size_t to) {
if (from >= v.size() || to >= v.size() || from == to)
return;
if (from < to) {
std::rotate(v.begin() + from, v.begin() + from + 1, v.begin() + to + 1);
} else {
std::rotate(v.begin() + to, v.begin() + from, v.begin() + from + 1);
}
}
template <typename T> void vector_erase_indices(std::vector<T>& v, const std::set<int>& indices) {
size_t i = 0;
v.erase(std::remove_if(v.begin(), v.end(), [&](const T&) { return indices.count(i++) > 0; }), v.end());
}
template <typename T> static inline void set_key_toggle(std::set<T>& set, T key) {
if (auto it = set.find(key); it != set.end())
set.erase(it);
else
set.insert(key);
}
template <typename T> static inline void set_list(std::set<T>& s, const T& key, bool isCtrl, bool isShift, T* lastSelected) {
if (isShift && lastSelected) {
s.clear();
T a = std::min(*lastSelected, key);
T b = std::max(*lastSelected, key);
for (T i = a; i <= b; i++)
s.insert(i);
} else if (isCtrl) {
set_key_toggle(s, key);
*lastSelected = key;
} else {
s = {key};
*lastSelected = key;
}
}
static inline mat4 quad_model_get(vec2 size = {}, vec2 position = {}, vec2 pivot = {}, vec2 scale = vec2(1.0f), float rotation = {}) {
vec2 scaleAbsolute = glm::abs(scale);
vec2 scaleSign = glm::sign(scale);
vec2 pivotScaled = pivot * scaleAbsolute;
vec2 sizeScaled = size * scaleAbsolute;
mat4 model(1.0f);
model = glm::translate(model, vec3(position - pivotScaled, 0.0f));
model = glm::translate(model, vec3(pivotScaled, 0.0f));
model = glm::scale(model, vec3(scaleSign, 1.0f));
model = glm::rotate(model, glm::radians(rotation), vec3(0, 0, 1));
model = glm::translate(model, vec3(-pivotScaled, 0.0f));
model = glm::scale(model, vec3(sizeScaled, 1.0f));
return model;
}
static inline mat4 quad_model_parent_get(vec2 position = {}, vec2 pivot = {}, vec2 scale = vec2(1.0f), float rotation = {}) {
vec2 scaleSign = glm::sign(scale);
vec2 scaleAbsolute = glm::abs(scale);
float handedness = (scaleSign.x * scaleSign.y) < 0.0f ? -1.0f : 1.0f;
mat4 local(1.0f);
local = glm::translate(local, vec3(pivot, 0.0f));
local = glm::scale(local, vec3(scaleSign, 1.0f));
local = glm::rotate(local, glm::radians(rotation) * handedness, vec3(0, 0, 1));
local = glm::translate(local, vec3(-pivot, 0.0f));
local = glm::scale(local, vec3(scaleAbsolute, 1.0f));
return glm::translate(mat4(1.0f), vec3(position, 0.0f)) * local;
}
#define DEFINE_STRING_TO_ENUM_FUNCTION(function, enumType, stringArray, count) \
static inline enumType function(const std::string& string) { return static_cast<enumType>(string_to_enum(string, stringArray, count)); };
#define DATATYPE_LIST \
X(TYPE_INT, int) \
X(TYPE_BOOL, bool) \
X(TYPE_FLOAT, float) \
X(TYPE_STRING, std::string) \
X(TYPE_IVEC2, ivec2) \
X(TYPE_IVEC2_WH, ivec2) \
X(TYPE_VEC2, vec2) \
X(TYPE_VEC2_WH, vec2) \
X(TYPE_VEC3, vec3) \
X(TYPE_VEC4, vec4)
enum DataType {
#define X(symbol, ctype) symbol,
DATATYPE_LIST
#undef X
};
#define DATATYPE_TO_CTYPE(dt) DATATYPE_CTYPE_##dt
#define X(symbol, ctype) typedef ctype DATATYPE_CTYPE_##symbol;
DATATYPE_LIST
#undef X
enum OriginType { ORIGIN_TOP_LEFT, ORIGIN_CENTER };
struct WorkingDirectory {
std::filesystem::path previous;
WorkingDirectory(const std::string& file) {
previous = std::filesystem::current_path();
working_directory_from_file_set(file);
}
~WorkingDirectory() { std::filesystem::current_path(previous); }
};

File diff suppressed because it is too large Load Diff

316
src/animation_preview.cpp Normal file
View File

@@ -0,0 +1,316 @@
#include "animation_preview.h"
#include <glm/gtc/type_ptr.hpp>
#include "imgui.h"
#include "math.h"
#include "tool.h"
#include "types.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::settings;
using namespace anm2ed::canvas;
using namespace anm2ed::playback;
using namespace anm2ed::resources;
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::animation_preview
{
constexpr auto TARGET_SIZE = vec2(32, 32);
constexpr auto PIVOT_SIZE = vec2(8, 8);
constexpr auto POINT_SIZE = vec2(4, 4);
constexpr auto NULL_RECT_SIZE = vec2(100);
constexpr auto TRIGGER_TEXT_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.5f);
AnimationPreview::AnimationPreview() : Canvas(vec2())
{
}
void AnimationPreview::update(DocumentManager& manager, Settings& settings, Resources& resources, Playback& playback)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto animation = document.animation_get();
auto& pan = document.previewPan;
auto& zoom = document.previewZoom;
auto& backgroundColor = settings.previewBackgroundColor;
auto& axesColor = settings.previewAxesColor;
auto& gridColor = settings.previewGridColor;
auto& gridSize = settings.previewGridSize;
auto& gridOffset = settings.previewGridOffset;
auto& zoomStep = settings.viewZoomStep;
auto& isGrid = settings.previewIsGrid;
auto& overlayTransparency = settings.previewOverlayTransparency;
auto& overlayIndex = document.overlayIndex;
auto& isRootTransform = settings.previewIsRootTransform;
auto& isPivots = settings.previewIsPivots;
auto& isAxes = settings.previewIsAxes;
auto& isIcons = settings.previewIsIcons;
auto& isAltIcons = settings.previewIsAltIcons;
auto& isBorder = settings.previewIsBorder;
auto& tool = settings.tool;
auto& shaderLine = resources.shaders[shader::LINE];
auto& shaderAxes = resources.shaders[shader::AXIS];
auto& shaderGrid = resources.shaders[shader::GRID];
auto& shaderTexture = resources.shaders[shader::TEXTURE];
if (ImGui::Begin("Animation Preview", &settings.windowIsAnimationPreview))
{
auto childSize = ImVec2(imgui::row_widget_width_get(4),
(ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2));
if (ImGui::BeginChild("##Grid Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::Checkbox("Grid", &isGrid);
ImGui::SameLine();
ImGui::ColorEdit4("Color", value_ptr(gridColor), ImGuiColorEditFlags_NoInputs);
ImGui::InputInt2("Size", value_ptr(gridSize));
ImGui::InputInt2("Offset", value_ptr(gridOffset));
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::InputFloat("Zoom", &zoom, zoomStep, zoomStep, "%.0f%%");
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button("Center View", widgetSize)) pan = vec2();
ImGui::SameLine();
ImGui::Button("Fit", widgetSize);
ImGui::TextUnformatted(std::format(POSITION_FORMAT, (int)mousePos.x, (int)mousePos.y).c_str());
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::SameLine();
ImGui::Checkbox("Axes", &isAxes);
ImGui::SameLine();
ImGui::ColorEdit4("Color", value_ptr(axesColor), ImGuiColorEditFlags_NoInputs);
std::vector<std::string> animationNames{};
animationNames.emplace_back("None");
for (auto& animation : anm2.animations.items)
animationNames.emplace_back(animation.name);
imgui::combo_strings("Overlay", &overlayIndex, animationNames);
ImGui::InputFloat("Alpha", &overlayTransparency, 0, 0, "%.0f");
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Helpers Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
auto helpersChildSize = ImVec2(imgui::row_widget_width_get(2), ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Helpers Child 1", helpersChildSize))
{
ImGui::Checkbox("Root Transform", &isRootTransform);
ImGui::Checkbox("Pivots", &isPivots);
ImGui::Checkbox("Icons", &isIcons);
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Helpers Child 2", helpersChildSize))
{
ImGui::Checkbox("Alt Icons", &isAltIcons);
ImGui::Checkbox("Border", &isBorder);
}
ImGui::EndChild();
}
ImGui::EndChild();
auto cursorScreenPos = ImGui::GetCursorScreenPos();
size_set(to_vec2(ImGui::GetContentRegionAvail()));
bind();
viewport_set();
clear(backgroundColor);
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
auto frameTime = reference.frameTime > -1 && !playback.isPlaying ? reference.frameTime : playback.time;
if (animation)
{
auto transform = transform_get(zoom, pan);
auto root = animation->rootAnimation.frame_generate(playback.time, anm2::ROOT);
if (isRootTransform)
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
if (isIcons && root.isVisible && animation->rootAnimation.isVisible)
{
auto rootTransform = transform * math::quad_model_get(TARGET_SIZE, root.position, TARGET_SIZE * 0.5f,
math::percent_to_unit(root.scale), root.rotation);
texture_render(shaderTexture, resources.icons[icon::TARGET].id, rootTransform, color::GREEN);
}
for (auto& id : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations.at(id);
if (!layerAnimation.isVisible) continue;
auto& layer = anm2.content.layers.at(id);
if (auto frame = layerAnimation.frame_generate(frameTime, anm2::LAYER); frame.isVisible)
{
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
if (!spritesheet) continue;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) continue;
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
math::percent_to_unit(frame.scale), frame.rotation);
auto inset = 0.5f / vec2(texture.size); // needed to avoid bleed
auto uvMin = frame.crop / vec2(texture.size) + inset;
auto uvMax = (frame.crop + frame.size) / vec2(texture.size) - inset;
auto vertices = math::uv_vertices_get(uvMin, uvMax);
texture_render(shaderTexture, texture.id, layerTransform, frame.tint, frame.offset, vertices.data());
if (isBorder) rect_render(shaderLine, layerTransform, color::RED);
if (isPivots)
{
auto pivotTransform =
transform * math::quad_model_get(PIVOT_SIZE, frame.position, PIVOT_SIZE * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color::RED);
}
}
}
if (isIcons)
{
for (auto& [id, nullAnimation] : animation->nullAnimations)
{
if (!nullAnimation.isVisible) continue;
auto& isShowRect = anm2.content.nulls[id].isShowRect;
if (auto frame = nullAnimation.frame_generate(frameTime, anm2::NULL_); frame.isVisible)
{
auto icon = isShowRect ? icon::POINT : icon::TARGET;
auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE;
auto& color = id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED : color::BLUE;
auto nullTransform = transform * math::quad_model_get(size, frame.position, size * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
texture_render(shaderTexture, resources.icons[icon].id, nullTransform, color);
if (isShowRect)
{
auto rectTransform =
transform * math::quad_model_get(NULL_RECT_SIZE, frame.position, NULL_RECT_SIZE * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
rect_render(shaderLine, rectTransform, color);
}
}
}
}
}
unbind();
ImGui::Image(texture, to_imvec2(size));
isPreviewHovered = ImGui::IsItemHovered();
if (animation && animation->triggers.isVisible)
{
if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); trigger.isVisible)
{
auto clipMin = ImGui::GetItemRectMin();
auto clipMax = ImGui::GetItemRectMax();
auto drawList = ImGui::GetWindowDrawList();
auto textPos = to_imvec2(to_vec2(cursorScreenPos) + to_vec2(ImGui::GetStyle().WindowPadding));
drawList->PushClipRect(clipMin, clipMax);
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE);
drawList->AddText(textPos, ImGui::GetColorU32(TRIGGER_TEXT_COLOR),
anm2.content.events.at(trigger.eventID).name.c_str());
ImGui::PopFont();
drawList->PopClipRect();
}
}
if (isPreviewHovered)
{
ImGui::SetKeyboardFocusHere(-1);
mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos));
auto isRound = settings.propertiesIsRound;
auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
auto isLeft = imgui::chord_repeating(ImGuiKey_LeftArrow);
auto isRight = imgui::chord_repeating(ImGuiKey_RightArrow);
auto isUp = imgui::chord_repeating(ImGuiKey_UpArrow);
auto isDown = imgui::chord_repeating(ImGuiKey_DownArrow);
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
auto mouseDelta = to_vec2(ImGui::GetIO().MouseDelta);
auto mouseWheel = ImGui::GetIO().MouseWheel;
auto isZoomIn = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomIn));
auto isZoomOut = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomOut));
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
auto frame = document.frame_get();
auto useTool = tool;
auto step = isMod ? step::FAST : step::NORMAL;
auto isClick = isMouseDown;
if (isMouseMiddleDown) useTool = tool::PAN;
if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE;
if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE;
switch (useTool)
{
case tool::PAN:
if (isClick || isMouseMiddleDown) pan += isRound ? vec2(ivec2(mouseDelta)) : mouseDelta;
break;
case tool::MOVE:
if (!frame) break;
if (isClick) frame->position = isRound ? vec2(ivec2(mousePos)) : mousePos;
if (isLeft) frame->position.x -= step;
if (isRight) frame->position.x += step;
if (isUp) frame->position.y -= step;
if (isDown) frame->position.y += step;
break;
case tool::SCALE:
if (!frame) break;
if (isClick) frame->scale += isRound ? vec2(ivec2(mouseDelta)) : mouseDelta;
break;
case tool::ROTATE:
if (!frame) break;
if (isClick) frame->rotation += isRound ? (int)mouseDelta.y : mouseDelta.y;
break;
default:
break;
}
if (mouseWheel != 0 || isZoomIn || isZoomOut)
zoom_set(zoom, pan, mousePos, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep);
}
}
ImGui::End();
}
}

21
src/animation_preview.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include "canvas.h"
#include "document_manager.h"
#include "playback.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::animation_preview
{
class AnimationPreview : public canvas::Canvas
{
bool isPreviewHovered{};
glm::vec2 mousePos{};
public:
AnimationPreview();
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback);
};
}

293
src/animations.cpp Normal file
View File

@@ -0,0 +1,293 @@
#include "animations.h"
#include <algorithm>
#include <ranges>
#include "imgui.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::animations
{
void Animations::update(DocumentManager& manager, Settings& settings, Resources& resources)
{
auto document = manager.get();
auto& anm2 = document->anm2;
auto& reference = document->reference;
auto& selection = document->selectedAnimations;
storage.UserData = &selection;
storage.AdapterSetItemSelected = imgui::external_storage_set;
if (ImGui::Begin("Animations", &settings.windowIsAnimations))
{
auto childSize = imgui::size_with_footer_get();
if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders))
{
ImGuiMultiSelectIO* io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(),
anm2.animations.items.size());
storage.ApplyRequests(io);
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
{
ImGui::PushID(i);
auto isDefault = anm2.animations.defaultAnimation == animation.name;
auto isReferenced = reference.animationIndex == i;
auto isSelected = selection.contains(i);
auto font = isDefault && isReferenced ? font::BOLD_ITALICS
: isDefault ? font::BOLD
: isReferenced ? font::ITALICS
: font::REGULAR;
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
ImGui::SetNextItemSelectionUserData(i);
if (imgui::selectable_input_text(animation.name,
std::format("###Document #{} Animation #{}", manager.selected, i),
animation.name, isSelected))
if (!isReferenced) reference = {(int)i};
ImGui::PopFont();
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(animation.name.c_str());
ImGui::PopFont();
if (isDefault)
{
ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::TextUnformatted("(Default Animation)");
ImGui::PopFont();
}
ImGui::TextUnformatted(std::format("Length: {}", animation.frameNum).c_str());
ImGui::TextUnformatted(std::format("Loop: {}", animation.isLoop).c_str());
ImGui::EndTooltip();
}
if (ImGui::BeginDragDropSource())
{
std::vector<int> sorted = {};
ImGui::SetDragDropPayload("Animation Drag Drop", sorted.data(), sorted.size() * sizeof(int));
for (auto& index : sorted)
ImGui::TextUnformatted(anm2.animations.items[index].name.c_str());
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget())
{
if (auto payload = ImGui::AcceptDragDropPayload("Animation Drag Drop"))
{
auto count = payload->DataSize / sizeof(int);
auto data = (int*)(payload->Data);
std::vector<int> indices(data, data + count);
//std::vector<int> destinationIndices = vector::indices_move(anm2.animations.items, indices, i);
selection.clear();
/*
for (const auto& index : destinationIndices)
selection.insert((int)index);
*/
}
ImGui::EndDragDropTarget();
}
ImGui::PopID();
}
io = ImGui::EndMultiSelect();
storage.ApplyRequests(io);
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(5);
imgui::shortcut(settings.shortcutAdd, true);
if (ImGui::Button("Add", widgetSize))
{
anm2::Animation animation;
if (anm2::Animation* referenceAnimation = document->animation_get())
{
for (auto [id, layerAnimation] : referenceAnimation->layerAnimations)
animation.layerAnimations[id] = anm2::Item();
animation.layerOrder = referenceAnimation->layerOrder;
for (auto [id, nullAnimation] : referenceAnimation->nullAnimations)
animation.nullAnimations[id] = anm2::Item();
}
animation.rootAnimation.frames.emplace_back(anm2::Frame());
auto index = selection.empty() ? (int)anm2.animations.items.size() - 1 : (int)std::ranges::max(selection) + 1;
anm2.animations.items.insert(anm2.animations.items.begin() + index, animation);
selection = {index};
reference = {index};
}
imgui::set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd);
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
{
imgui::shortcut(settings.shortcutDuplicate, true);
if (ImGui::Button("Duplicate", widgetSize))
{
auto duplicated = selection;
auto duplicatedEnd = std::ranges::max(duplicated);
for (auto& id : duplicated)
{
anm2.animations.items.insert(anm2.animations.items.begin() + duplicatedEnd, anm2.animations.items[id]);
selection.insert(++duplicatedEnd);
selection.erase(id);
}
}
imgui::set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate);
ImGui::SameLine();
ImGui::BeginDisabled(selection.size() != 1);
{
if (ImGui::Button("Merge", widgetSize))
{
ImGui::OpenPopup("Merge Animations");
mergeSelection.clear();
mergeTarget = *selection.begin();
}
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut(
"Open the merge popup.\nUsing the shortcut will merge the animations with\nthe last "
"configured merge settings.",
settings.shortcutMerge);
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove, true);
if (ImGui::Button("Remove", widgetSize))
{
/*
auto selectionErase = set::to_size_t(selection);
if (selectionErase.contains(document->reference.animationIndex)) document->reference.animationIndex = -1;
vector::range_erase(anm2.animations.items, selectionErase);
*/
selection.clear();
}
imgui::set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate);
ImGui::SameLine();
ImGui::BeginDisabled(selection.size() != 1);
{
imgui::shortcut(settings.shortcutDefault, true);
if (ImGui::Button("Default", widgetSize))
anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name;
imgui::set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDuplicate);
}
ImGui::EndDisabled();
}
ImGui::EndDisabled();
auto viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * 0.5f));
if (ImGui::BeginPopupModal("Merge Animations", nullptr, ImGuiWindowFlags_NoResize))
{
auto merge_close = [&]()
{
mergeSelection.clear();
ImGui::CloseCurrentPopup();
};
auto& type = settings.mergeType;
auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter;
mergeStorage.UserData = &mergeSelection;
mergeStorage.AdapterSetItemSelected = imgui::external_storage_set;
auto footerSize = ImVec2(0, imgui::footer_height_get());
auto optionsSize = imgui::child_size_get(2, true);
auto deleteAfterSize = imgui::child_size_get();
auto animationsSize =
ImVec2(0, ImGui::GetContentRegionAvail().y -
(optionsSize.y + deleteAfterSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 3));
if (ImGui::BeginChild("Animations", animationsSize, ImGuiChildFlags_Borders))
{
ImGuiMultiSelectIO* io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, mergeSelection.size(),
anm2.animations.items.size());
mergeStorage.ApplyRequests(io);
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
{
auto isSelected = mergeSelection.contains(i);
ImGui::PushID(i);
ImGui::SetNextItemSelectionUserData(i);
ImGui::Selectable(animation.name.c_str(), isSelected);
ImGui::PopID();
}
io = ImGui::EndMultiSelect();
mergeStorage.ApplyRequests(io);
}
ImGui::EndChild();
if (ImGui::BeginChild("Merge Options", optionsSize, ImGuiChildFlags_Borders))
{
auto size = ImVec2(optionsSize.x * 0.5f, optionsSize.y - ImGui::GetStyle().WindowPadding.y * 2);
if (ImGui::BeginChild("Merge Options 1", size))
{
ImGui::RadioButton("Append Frames", &type, merge::APPEND);
ImGui::RadioButton("Prepend Frames", &type, merge::PREPEND);
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("Merge Options 2", size))
{
ImGui::RadioButton("Replace Frames", &type, merge::REPLACE);
ImGui::RadioButton("Ignore Frames", &type, merge::IGNORE);
}
ImGui::EndChild();
}
ImGui::EndChild();
if (ImGui::BeginChild("Merge Delete After", deleteAfterSize, ImGuiChildFlags_Borders))
ImGui::Checkbox("Delete Animations After", &isDeleteAnimationsAfter);
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button("Merge", widgetSize))
{
/*
std::set<int> sources = set::to_set(mergeSelection);
const auto merged = anm2.animations.merge(mergeTarget, sources, (MergeType)type, isDeleteAnimationsAfter);
selection = {merged};
reference = {merged};
*/
merge_close();
}
ImGui::SameLine();
if (ImGui::Button("Close", widgetSize)) merge_close();
ImGui::EndPopup();
}
}
ImGui::End();
}
}

20
src/animations.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "document_manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::animations
{
class Animations
{
ImGuiSelectionExternalStorage mergeStorage{};
ImGuiSelectionExternalStorage storage{};
std::set<int> mergeSelection{};
int mergeTarget{};
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,266 +1,238 @@
#pragma once
#include "resources.h"
#include <filesystem>
#include <map>
#include <set>
#include <string>
#include <tinyxml2/tinyxml2.h>
#include <vector>
#define ANM2_SCALE_CONVERT(x) ((float)x / 100.0f)
#define ANM2_TINT_CONVERT(x) ((float)x / 255.0f)
#include "texture.h"
#include "types.h"
#define ANM2_FPS_MIN 0
#define ANM2_FPS_DEFAULT 30
#define ANM2_FPS_MAX 120
#define ANM2_FRAME_NUM_MIN 1
#define ANM2_FRAME_NUM_MAX 1000000
#define ANM2_FRAME_DELAY_MIN 1
#define ANM2_STRING_MAX 0xFF
namespace anm2ed::anm2
{
constexpr auto FRAME_NUM_MIN = 1;
constexpr auto FRAME_NUM_MAX = 100000000;
constexpr auto FRAME_DELAY_MIN = 1;
constexpr auto FRAME_DELAY_MAX = FRAME_NUM_MAX;
constexpr auto FPS_MIN = 1;
constexpr auto FPS_MAX = 120;
#define ANM2_EMPTY_ERROR "No path given for anm2"
#define ANM2_READ_ERROR "Failed to read anm2 from file: {}"
#define ANM2_PARSE_ERROR "Failed to parse anm2: {} ({})"
#define ANM2_FRAME_PARSE_ERROR "Failed to parse frame: {} ({})"
#define ANM2_ANIMATION_PARSE_ERROR "Failed to parse frame: {} ({})"
#define ANM2_READ_INFO "Read anm2 from file: {}"
#define ANM2_WRITE_ERROR "Failed to write anm2 to file: {}"
#define ANM2_WRITE_INFO "Wrote anm2 to file: {}"
#define ANM2_CREATED_ON_FORMAT "%d-%B-%Y %I:%M:%S %p"
constexpr auto MERGED_STRING = "(Merged)";
#define ANM2_ANIMATION_DEFAULT "New Animation"
#define ANM2_EXTENSION "anm2"
#define ANM2_SPRITESHEET_EXTENSION "png"
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
#define ANM2_ELEMENT_LIST \
X(ANIMATED_ACTOR, "AnimatedActor") \
X(INFO, "Info") \
X(CONTENT, "Content") \
X(SPRITESHEETS, "Spritesheets") \
X(SPRITESHEET, "Spritesheet") \
X(LAYERS, "Layers") \
X(LAYER, "Layer") \
X(NULLS, "Nulls") \
X(NULL, "Null") \
X(EVENTS, "Events") \
X(EVENT, "Event") \
X(ANIMATIONS, "Animations") \
X(ANIMATION, "Animation") \
X(ROOT_ANIMATION, "RootAnimation") \
X(FRAME, "Frame") \
X(LAYER_ANIMATIONS, "LayerAnimations") \
X(LAYER_ANIMATION, "LayerAnimation") \
X(NULL_ANIMATIONS, "NullAnimations") \
X(NULL_ANIMATION, "NullAnimation") \
X(TRIGGERS, "Triggers") \
X(TRIGGER, "Trigger")
class Reference
{
public:
int animationIndex{-1};
Type itemType{NONE};
int itemID{-1};
int frameIndex{-1};
int frameTime{-1};
typedef enum {
#define X(name, str) ANM2_ELEMENT_##name,
ANM2_ELEMENT_LIST
#undef X
ANM2_ELEMENT_COUNT
} Anm2Element;
void previous_frame(int max = FRAME_NUM_MAX - 1);
void next_frame(int max = FRAME_NUM_MAX - 1);
auto operator<=>(const Reference&) const = default;
};
const inline char* ANM2_ELEMENT_STRINGS[] = {
#define X(name, str) str,
ANM2_ELEMENT_LIST
#undef X
};
class Info
{
public:
std::string createdBy{"robot"};
std::string createdOn{};
int fps = 30;
int version{};
DEFINE_STRING_TO_ENUM_FUNCTION(ANM2_ELEMENT_STRING_TO_ENUM, Anm2Element, ANM2_ELEMENT_STRINGS, ANM2_ELEMENT_COUNT)
Info();
Info(tinyxml2::XMLElement* element);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
};
#define ANM2_ATTRIBUTE_LIST \
X(CREATED_BY, "CreatedBy") \
X(CREATED_ON, "CreatedOn") \
X(VERSION, "Version") \
X(FPS, "Fps") \
X(ID, "Id") \
X(PATH, "Path") \
X(NAME, "Name") \
X(SPRITESHEET_ID, "SpritesheetId") \
X(SHOW_RECT, "ShowRect") \
X(DEFAULT_ANIMATION, "DefaultAnimation") \
X(FRAME_NUM, "FrameNum") \
X(LOOP, "Loop") \
X(X_POSITION, "XPosition") \
X(Y_POSITION, "YPosition") \
X(X_PIVOT, "XPivot") \
X(Y_PIVOT, "YPivot") \
X(X_CROP, "XCrop") \
X(Y_CROP, "YCrop") \
X(WIDTH, "Width") \
X(HEIGHT, "Height") \
X(X_SCALE, "XScale") \
X(Y_SCALE, "YScale") \
X(DELAY, "Delay") \
X(VISIBLE, "Visible") \
X(RED_TINT, "RedTint") \
X(GREEN_TINT, "GreenTint") \
X(BLUE_TINT, "BlueTint") \
X(ALPHA_TINT, "AlphaTint") \
X(RED_OFFSET, "RedOffset") \
X(GREEN_OFFSET, "GreenOffset") \
X(BLUE_OFFSET, "BlueOffset") \
X(ROTATION, "Rotation") \
X(INTERPOLATED, "Interpolated") \
X(LAYER_ID, "LayerId") \
X(NULL_ID, "NullId") \
X(EVENT_ID, "EventId") \
X(AT_FRAME, "AtFrame")
class Spritesheet
{
public:
std::filesystem::path path{};
texture::Texture texture;
typedef enum {
#define X(name, str) ANM2_ATTRIBUTE_##name,
ANM2_ATTRIBUTE_LIST
#undef X
ANM2_ATTRIBUTE_COUNT
} Anm2Attribute;
Spritesheet();
Spritesheet(tinyxml2::XMLElement* element, int& id);
Spritesheet(const std::string& directory, const std::string& path = {});
bool save(const std::string& directory, const std::string& path = {});
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
void reload(const std::string& directory);
bool is_valid();
};
static const char* ANM2_ATTRIBUTE_STRINGS[] = {
#define X(name, str) str,
ANM2_ATTRIBUTE_LIST
#undef X
};
class Layer
{
public:
std::string name{"New Layer"};
int spritesheetID{};
DEFINE_STRING_TO_ENUM_FUNCTION(ANM2_ATTRIBUTE_STRING_TO_ENUM, Anm2Attribute, ANM2_ATTRIBUTE_STRINGS, ANM2_ATTRIBUTE_COUNT)
Layer();
Layer(tinyxml2::XMLElement* element, int& id);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
};
enum Anm2Type { ANM2_NONE, ANM2_ROOT, ANM2_LAYER, ANM2_NULL, ANM2_TRIGGER, ANM2_COUNT };
class Null
{
public:
std::string name{"New Null"};
bool isShowRect{};
struct Anm2Spritesheet {
std::string path{};
Texture texture;
std::vector<uint8_t> pixels;
Null();
Null(tinyxml2::XMLElement* element, int& id);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
};
auto operator<=>(const Anm2Spritesheet&) const = default;
};
class Event
{
public:
std::string name{"New Event"};
struct Anm2Layer {
std::string name = "New Layer";
int spritesheetID{};
Event();
Event(tinyxml2::XMLElement* element, int& id);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
};
auto operator<=>(const Anm2Layer&) const = default;
};
struct Content
{
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
struct Anm2Null {
std::string name = "New Null";
bool isShowRect = false;
Content();
auto operator<=>(const Anm2Null&) const = default;
};
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
Content(tinyxml2::XMLElement* element);
bool spritesheet_add(const std::string& directory, const std::string& path, int& id);
void spritesheet_remove(int& id);
std::set<int> spritesheets_unused();
void layer_add(int& id);
void null_add(int& id);
void event_add(int& id);
};
struct Anm2Event {
std::string name = "New Event";
#define MEMBERS \
X(isVisible, bool, true) \
X(isInterpolated, bool, false) \
X(rotation, float, 0.0f) \
X(delay, int, FRAME_DELAY_MIN) \
X(atFrame, int, -1) \
X(eventID, int, -1) \
X(pivot, glm::vec2, {}) \
X(crop, glm::vec2, {}) \
X(position, glm::vec2, {}) \
X(size, glm::vec2, {}) \
X(scale, glm::vec2, glm::vec2(100.0f)) \
X(offset, glm::vec3, types::color::TRANSPARENT) \
X(tint, glm::vec4, types::color::WHITE)
auto operator<=>(const Anm2Event&) const = default;
};
#define ANM2_FRAME_MEMBERS \
X(isVisible, bool, true) \
X(isInterpolated, bool, false) \
X(rotation, float, 0.0f) \
X(delay, int, ANM2_FRAME_DELAY_MIN) \
X(atFrame, int, INDEX_NONE) \
X(eventID, int, ID_NONE) \
X(crop, vec2, {}) \
X(pivot, vec2, {}) \
X(position, vec2, {}) \
X(size, vec2, {}) \
X(scale, vec2, {100, 100}) \
X(offsetRGB, vec3, COLOR_OFFSET_NONE) \
X(tintRGBA, vec4, COLOR_OPAQUE)
struct Anm2Frame {
class Frame
{
public:
#define X(name, type, ...) type name = __VA_ARGS__;
ANM2_FRAME_MEMBERS
MEMBERS
#undef X
auto operator<=>(const Anm2Frame&) const = default;
};
struct Anm2FrameChange {
Frame();
Frame(tinyxml2::XMLElement* element, Type type);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, Type type);
void shorten();
void extend();
};
struct FrameChange
{
#define X(name, type, ...) std::optional<type> name{};
ANM2_FRAME_MEMBERS
MEMBERS
#undef X
};
};
struct Anm2Item {
bool isVisible = true;
std::vector<Anm2Frame> frames;
#undef MEMBERS
auto operator<=>(const Anm2Item&) const = default;
};
class Item
{
public:
std::vector<Frame> frames{};
bool isVisible{true};
struct Anm2Animation {
int frameNum = ANM2_FRAME_NUM_MIN;
std::string name = ANM2_ANIMATION_DEFAULT;
bool isLoop = true;
Anm2Item rootAnimation;
std::unordered_map<int, Anm2Item> layerAnimations;
std::vector<int> layerOrder;
std::map<int, Anm2Item> nullAnimations;
Anm2Item triggers;
Item();
auto operator<=>(const Anm2Animation&) const = default;
};
Item(tinyxml2::XMLElement* element, Type type, int* id = nullptr);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, Type type, int id = -1);
int length(Type type);
Frame frame_generate(float time, Type type);
};
struct Anm2 {
std::string path{};
std::string createdBy = "robot";
std::string createdOn{};
std::string defaultAnimation = ANM2_ANIMATION_DEFAULT;
std::map<int, Anm2Spritesheet> spritesheets;
std::map<int, Anm2Layer> layers;
std::map<int, Anm2Null> nulls;
std::map<int, Anm2Event> events;
std::vector<Anm2Animation> animations;
int fps = ANM2_FPS_DEFAULT;
int version{};
class Animation
{
public:
std::string name{"New Animation"};
int frameNum{FRAME_NUM_MIN};
bool isLoop{true};
Item rootAnimation;
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers;
auto operator<=>(const Anm2&) const = default;
};
Animation();
Animation(tinyxml2::XMLElement* element);
Item* item_get(Type type, int id = -1);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
int length();
};
struct Anm2Reference {
int animationIndex = ID_NONE;
Anm2Type itemType = ANM2_NONE;
int itemID = ID_NONE;
int frameIndex = INDEX_NONE;
float time = VALUE_NONE;
auto operator<=>(const Anm2Reference&) const = default;
};
struct Animations
{
std::string defaultAnimation{};
std::vector<Animation> items{};
enum Anm2MergeType { ANM2_MERGE_APPEND, ANM2_MERGE_REPLACE, ANM2_MERGE_PREPEND, ANM2_MERGE_IGNORE };
Animations();
enum Anm2ChangeType { ANM2_CHANGE_ADD, ANM2_CHANGE_SUBTRACT, ANM2_CHANGE_SET };
Animations(tinyxml2::XMLElement* element);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
int length();
int merge(int target, std::set<int>& sources, types::merge::Type type, bool isDeleteAfter = true);
};
enum OnionskinDrawOrder { ONIONSKIN_BELOW, ONIONSKIN_ABOVE };
class Anm2
{
bool isValid{false};
Anm2Animation* anm2_animation_from_reference(Anm2* self, Anm2Reference reference);
Anm2Frame* anm2_frame_add(Anm2* self, Anm2Frame* frame, Anm2Reference reference);
Anm2Frame* anm2_frame_from_reference(Anm2* self, Anm2Reference reference);
Anm2Item* anm2_item_from_reference(Anm2* self, Anm2Reference reference);
bool anm2_animations_deserialize_from_xml(std::vector<Anm2Animation>& animations, const std::string& xml);
bool anm2_deserialize(Anm2* self, const std::string& path, bool isTextures = true);
bool anm2_frame_deserialize_from_xml(Anm2Frame* frame, const std::string& xml);
bool anm2_serialize(Anm2* self, const std::string& path);
int anm2_animation_add(Anm2* self, Anm2Animation* animation = nullptr, int index = INDEX_NONE);
int anm2_animation_length_get(Anm2Animation* self);
int anm2_frame_index_from_time(Anm2* self, Anm2Reference reference, float time);
int anm2_layer_add(Anm2* self);
int anm2_null_add(Anm2* self);
vec4 anm2_animation_rect_get(Anm2* anm2, Anm2Reference reference, bool isRootTransform);
void anm2_animation_layer_animation_add(Anm2Animation* animation, int id);
void anm2_animation_layer_animation_remove(Anm2Animation* animation, int id);
void anm2_animation_length_set(Anm2Animation* self);
void anm2_animation_merge(Anm2* self, int animationID, std::set<int> mergeIndices, Anm2MergeType type);
void anm2_animation_null_animation_add(Anm2Animation* animation, int id);
void anm2_animation_null_animation_remove(Anm2Animation* animation, int id);
void anm2_animations_remove(Anm2* self, const std::set<int> indices);
void anm2_animation_serialize_to_string(Anm2Animation* animation, std::string* string);
void anm2_frame_bake(Anm2* self, Anm2Reference reference, int interval, bool isRoundScale, bool isRoundRotation);
void anm2_frame_from_time(Anm2* self, Anm2Frame* frame, Anm2Reference reference, float time);
void anm2_frame_remove(Anm2* self, Anm2Reference reference);
void anm2_frame_serialize_to_string(Anm2Frame* frame, Anm2Type type, std::string* string);
void anm2_free(Anm2* self);
void anm2_generate_from_grid(Anm2* self, Anm2Reference reference, vec2 startPosition, vec2 size, vec2 pivot, int columns, int count, int delay);
void anm2_item_frame_set(Anm2* self, Anm2Reference reference, const Anm2FrameChange& change, Anm2ChangeType changeType, int start, int count);
void anm2_layer_remove(Anm2* self, int id);
void anm2_new(Anm2* self);
void anm2_null_remove(Anm2* self, int id);
void anm2_scale(Anm2* self, float scale);
void anm2_spritesheet_texture_pixels_download(Anm2* self);
void anm2_spritesheet_texture_pixels_upload(Anm2* self);
float anm2_time_from_reference(Anm2* self, Anm2Reference reference);
public:
Info info{};
Content content{};
Animations animations{};
Anm2();
bool serialize(const std::string& path, std::string* errorString = nullptr);
std::string to_string();
Anm2(const std::string& path, std::string* errorString = nullptr);
uint64_t hash();
Animation* animation_get(Reference& reference);
Item* item_get(Reference& reference);
Frame* frame_get(Reference& reference);
bool spritesheet_add(const std::string& directory, const std::string& path, int& id);
Spritesheet* spritesheet_get(int id);
void spritesheet_remove(int id);
std::set<int> spritesheets_unused();
void layer_add(int& id);
void null_add(int& id);
void event_add(int& id);
std::set<int> events_unused();
std::set<int> layers_unused();
std::set<int> nulls_unused();
};
}

View File

@@ -1,224 +1,282 @@
#include "canvas.h"
static void _canvas_framebuffer_set(Canvas* self, const ivec2& size) {
self->size = size;
self->previousSize = size;
#include "math.h"
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
using namespace glm;
using namespace anm2ed::shader;
glBindTexture(GL_TEXTURE_2D, self->framebuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self->size.x, self->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
namespace anm2ed::canvas
{
constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f};
constexpr float GRID_VERTICES[] = {-1.f, -1.f, 0.f, 0.f, 3.f, -1.f, 2.f, 0.f, -1.f, 3.f, 0.f, 2.f};
constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->framebuffer, 0);
Canvas::Canvas() = default;
glBindRenderbuffer(GL_RENDERBUFFER, self->rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, self->size.x, self->size.y);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, self->rbo);
Canvas::Canvas(vec2 size)
{
this->size = size;
previousSize = size;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
// Axis
glGenVertexArrays(1, &axisVAO);
glGenBuffers(1, &axisVBO);
void canvas_init(Canvas* self, const ivec2& size) {
// Axis
glGenVertexArrays(1, &self->axisVAO);
glGenBuffers(1, &self->axisVBO);
glBindVertexArray(axisVAO);
glBindVertexArray(self->axisVAO);
glBindBuffer(GL_ARRAY_BUFFER, axisVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(AXIS_VERTICES), AXIS_VERTICES, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, self->axisVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_AXIS_VERTICES), CANVAS_AXIS_VERTICES, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(float), (void*)0);
// Grid
glGenVertexArrays(1, &gridVAO);
glBindVertexArray(gridVAO);
// Grid
glGenVertexArrays(1, &self->gridVAO);
glGenBuffers(1, &self->gridVBO);
glGenBuffers(1, &gridVBO);
glBindBuffer(GL_ARRAY_BUFFER, gridVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GRID_VERTICES), GRID_VERTICES, GL_STATIC_DRAW);
glBindVertexArray(self->gridVAO);
glBindBuffer(GL_ARRAY_BUFFER, self->gridVBO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)(sizeof(float) * 2));
// Rect
glGenVertexArrays(1, &self->rectVAO);
glGenBuffers(1, &self->rectVBO);
glBindVertexArray(0);
glBindVertexArray(self->rectVAO);
// Rect
glGenVertexArrays(1, &rectVAO);
glGenBuffers(1, &rectVBO);
glBindBuffer(GL_ARRAY_BUFFER, self->rectVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_RECT_VERTICES), CANVAS_RECT_VERTICES, GL_STATIC_DRAW);
glBindVertexArray(rectVAO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, rectVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(RECT_VERTICES), RECT_VERTICES, GL_STATIC_DRAW);
// Grid
glGenVertexArrays(1, &self->gridVAO);
glBindVertexArray(self->gridVAO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glGenBuffers(1, &self->gridVBO);
glBindBuffer(GL_ARRAY_BUFFER, self->gridVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_GRID_VERTICES), CANVAS_GRID_VERTICES, GL_STATIC_DRAW);
// Texture
glGenVertexArrays(1, &textureVAO);
glGenBuffers(1, &textureVBO);
glGenBuffers(1, &textureEBO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (void*)0);
glBindVertexArray(textureVAO);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 4 * 4, nullptr, GL_DYNAMIC_DRAW);
// Texture
glGenVertexArrays(1, &self->textureVAO);
glGenBuffers(1, &self->textureVBO);
glGenBuffers(1, &self->textureEBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, textureEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(TEXTURE_INDICES), TEXTURE_INDICES, GL_DYNAMIC_DRAW);
glBindVertexArray(self->textureVAO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, self->textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 4 * 4, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self->textureEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GL_TEXTURE_INDICES), GL_TEXTURE_INDICES, GL_DYNAMIC_DRAW);
glBindVertexArray(0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
// Framebuffer
glGenTextures(1, &texture);
glGenFramebuffers(1, &fbo);
glGenRenderbuffers(1, &rbo);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// Framebuffer
glGenTextures(1, &self->framebuffer);
glGenFramebuffers(1, &self->fbo);
glGenRenderbuffers(1, &self->rbo);
_canvas_framebuffer_set(self, size);
self->isInit = true;
}
mat4 canvas_transform_get(Canvas* self, vec2 pan, float zoom, OriginType origin) {
float zoomFactor = PERCENT_TO_UNIT(zoom);
mat4 projection = glm::ortho(0.0f, (float)self->size.x, 0.0f, (float)self->size.y, -1.0f, 1.0f);
mat4 view = mat4{1.0f};
vec2 size = vec2(self->size.x, self->size.y);
switch (origin) {
case ORIGIN_TOP_LEFT:
view = glm::translate(view, vec3(pan, 0.0f));
break;
default:
view = glm::translate(view, vec3((size * 0.5f) + pan, 0.0f));
break;
framebuffer_set();
}
view = glm::scale(view, vec3(zoomFactor, zoomFactor, 1.0f));
Canvas::~Canvas()
{
if (!is_valid()) return;
return projection * view;
glDeleteFramebuffers(1, &fbo);
glDeleteRenderbuffers(1, &rbo);
glDeleteTextures(1, &texture);
glDeleteVertexArrays(1, &axisVAO);
glDeleteBuffers(1, &axisVBO);
glDeleteVertexArrays(1, &gridVAO);
glDeleteBuffers(1, &gridVBO);
glDeleteVertexArrays(1, &rectVAO);
glDeleteBuffers(1, &rectVBO);
}
bool Canvas::is_valid()
{
return fbo != 0;
}
void Canvas::framebuffer_set()
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Canvas::framebuffer_resize_check()
{
if (previousSize != size)
{
framebuffer_set();
previousSize = size;
}
}
void Canvas::size_set(vec2 size)
{
this->size = size;
framebuffer_resize_check();
}
mat4 Canvas::transform_get(float zoom, vec2 pan)
{
auto zoomFactor = math::percent_to_unit(zoom);
auto projection = glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f);
auto view = mat4{1.0f};
view = glm::translate(view, vec3((size * 0.5f) + pan, 0.0f));
view = glm::scale(view, vec3(zoomFactor, zoomFactor, 1.0f));
return projection * view;
}
void Canvas::axes_render(Shader& shader, float zoom, vec2 pan, vec4 color)
{
auto originNDC = transform_get(zoom, pan) * vec4(0.0f, 0.0f, 0.0f, 1.0f);
originNDC /= originNDC.w;
glUseProgram(shader.id);
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_ORIGIN_NDC), originNDC.x, originNDC.y);
glBindVertexArray(axisVAO);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_AXIS), 0);
glDrawArrays(GL_LINES, 0, 2);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_AXIS), 1);
glDrawArrays(GL_LINES, 0, 2);
glBindVertexArray(0);
glUseProgram(0);
}
void Canvas::grid_render(Shader& shader, float zoom, vec2 pan, ivec2 size, ivec2 offset, vec4 color)
{
auto zoomFactor = math::percent_to_unit(zoom);
glUseProgram(shader.id);
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_VIEW_SIZE), this->size.x, this->size.y);
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_PAN), pan.x, pan.y);
glUniform1f(glGetUniformLocation(shader.id, shader::UNIFORM_ZOOM), zoomFactor);
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_SIZE), (float)size.x, (float)size.y);
glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_OFFSET), (float)offset.x, (float)offset.y);
glUniform4f(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), color.r, color.g, color.b, color.a);
glBindVertexArray(gridVAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
}
void Canvas::texture_render(Shader& shader, GLuint& texture, mat4& transform, vec4 tint, vec3 colorOffset,
float* vertices)
{
glUseProgram(shader.id);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_TEXTURE), 0);
glUniform3fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR_OFFSET), 1, value_ptr(colorOffset));
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TINT), 1, value_ptr(tint));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glBindVertexArray(textureVAO);
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
}
void Canvas::rect_render(Shader& shader, mat4& transform, vec4 color)
{
glUseProgram(shader.id);
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
glBindVertexArray(rectVAO);
glDrawArrays(GL_LINE_LOOP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
void Canvas::viewport_set()
{
glViewport(0, 0, size.x, size.y);
}
void Canvas::clear(vec4& color)
{
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Canvas::bind()
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
}
void Canvas::unbind()
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Canvas::zoom_set(float& zoom, vec2& pan, vec2& focus, float step)
{
auto zoomFactor = math::percent_to_unit(zoom);
float newZoom = glm::clamp(math::round_nearest_multiple(zoom + step, step), canvas::ZOOM_MIN, canvas::ZOOM_MAX);
if (newZoom != zoom)
{
float newZoomFactor = math::percent_to_unit(newZoom);
pan += focus * (zoomFactor - newZoomFactor);
zoom = newZoom;
}
}
vec2 Canvas::position_translate(float& zoom, vec2& pan, vec2 position)
{
auto zoomFactor = math::percent_to_unit(zoom);
return (position - pan - (size * 0.5f)) / zoomFactor;
}
}
void canvas_clear(vec4& color) {
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT);
}
void canvas_viewport_set(Canvas* self) { glViewport(0, 0, (int)self->size.x, (int)self->size.y); }
void canvas_framebuffer_resize_check(Canvas* self) {
if (self->previousSize != self->size)
_canvas_framebuffer_set(self, self->size);
}
void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color) {
mat4 inverseTransform = glm::inverse(transform);
glUseProgram(shader);
glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_MODEL), 1, GL_FALSE, glm::value_ptr(inverseTransform));
glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_SIZE), size.x, size.y);
glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_OFFSET), offset.x, offset.y);
glUniform4f(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), color.r, color.g, color.b, color.a);
glBindVertexArray(self->gridVAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
}
void canvas_texture_draw(Canvas* self, GLuint& shader, GLuint& texture, mat4& transform, const float* vertices, vec4 tint, vec3 colorOffset) {
glUseProgram(shader);
glBindVertexArray(self->textureVAO);
glBindBuffer(GL_ARRAY_BUFFER, self->textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CANVAS_TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(shader, SHADER_UNIFORM_TEXTURE), 0);
glUniform3fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR_OFFSET), 1, value_ptr(colorOffset));
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TINT), 1, value_ptr(tint));
glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
}
void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color) {
glUseProgram(shader);
glBindVertexArray(self->rectVAO);
glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform));
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color));
glDrawArrays(GL_LINE_LOOP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color) {
vec4 originNDC = transform * vec4(0.0f, 0.0f, 0.0f, 1.0f);
originNDC /= originNDC.w;
glUseProgram(shader);
glBindVertexArray(self->axisVAO);
glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color));
glUniform2f(glGetUniformLocation(shader, SHADER_UNIFORM_ORIGIN_NDC), originNDC.x, originNDC.y);
glUniform1i(glGetUniformLocation(shader, SHADER_UNIFORM_AXIS), 0);
glDrawArrays(GL_LINES, 0, 2);
glUniform1i(glGetUniformLocation(shader, SHADER_UNIFORM_AXIS), 1);
glDrawArrays(GL_LINES, 0, 2);
glBindVertexArray(0);
glUseProgram(0);
}
void canvas_bind(Canvas* self) { glBindFramebuffer(GL_FRAMEBUFFER, self->fbo); }
void canvas_unbind(void) { glBindFramebuffer(GL_FRAMEBUFFER, 0); }
void canvas_free(Canvas* self) {
if (!self->isInit)
return;
glDeleteFramebuffers(1, &self->fbo);
glDeleteRenderbuffers(1, &self->rbo);
glDeleteTextures(1, &self->framebuffer);
glDeleteVertexArrays(1, &self->axisVAO);
glDeleteVertexArrays(1, &self->rectVAO);
glDeleteVertexArrays(1, &self->gridVAO);
glDeleteVertexArrays(1, &self->textureVAO);
glDeleteBuffers(1, &self->axisVBO);
glDeleteBuffers(1, &self->rectVBO);
glDeleteBuffers(1, &self->gridVBO);
glDeleteBuffers(1, &self->textureVBO);
glDeleteBuffers(1, &self->textureEBO);
}

View File

@@ -1,63 +1,55 @@
#pragma once
#include "resources.h"
#include <glad/glad.h>
#include <glm/glm.hpp>
#define CANVAS_ZOOM_MIN 1.0f
#define CANVAS_ZOOM_MAX 2000.0f
#define CANVAS_ZOOM_DEFAULT 100.0f
#define CANVAS_ZOOM_STEP 100.0f
#define CANVAS_GRID_MIN 1
#define CANVAS_GRID_MAX 1000
#define CANVAS_GRID_DEFAULT 32
#include "shader.h"
const inline vec2 CANVAS_PIVOT_SIZE = {4, 4};
const inline vec2 CANVAS_SCALE_DEFAULT = {1.0f, 1.0f};
namespace anm2ed::canvas
{
constexpr float TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f, 1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f};
const inline float CANVAS_AXIS_VERTICES[] = {-1.0f, 1.0f};
constexpr auto ZOOM_MIN = 1.0f;
constexpr auto ZOOM_MAX = 2000.0f;
constexpr auto POSITION_FORMAT = "Position: ({:8} {:8})";
const inline float CANVAS_GRID_VERTICES[] = {-1.0f, -1.0f, 3.0f, -1.0f, -1.0f, 3.0f};
class Canvas
{
public:
GLuint fbo{};
GLuint rbo{};
GLuint axisVAO{};
GLuint axisVBO{};
GLuint rectVAO{};
GLuint rectVBO{};
GLuint gridVAO{};
GLuint gridVBO{};
GLuint textureVAO{};
GLuint textureVBO{};
GLuint textureEBO{};
GLuint texture{};
glm::vec2 previousSize{};
glm::vec2 size{};
const inline float CANVAS_RECT_VERTICES[] = {0, 0, 1, 0, 1, 1, 0, 1};
const inline float CANVAS_TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f, 1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f};
struct Canvas {
GLuint fbo{};
GLuint rbo{};
GLuint axisVAO{};
GLuint axisVBO{};
GLuint rectVAO{};
GLuint rectVBO{};
GLuint gridVAO{};
GLuint gridVBO{};
GLuint framebuffer{};
GLuint textureVAO{};
GLuint textureVBO{};
GLuint textureEBO{};
ivec2 size{};
ivec2 previousSize{};
bool isInit = false;
};
#define UV_VERTICES(uvMin, uvMax) {0, 0, uvMin.x, uvMin.y, 1, 0, uvMax.x, uvMin.y, 1, 1, uvMax.x, uvMax.y, 0, 1, uvMin.x, uvMax.y}
#define ATLAS_UV_MIN(type) (ATLAS_POSITION(type) / TEXTURE_ATLAS_SIZE)
#define ATLAS_UV_MAX(type) ((ATLAS_POSITION(type) + ATLAS_SIZE(type)) / TEXTURE_ATLAS_SIZE)
#define ATLAS_UV_ARGS(type) ATLAS_UV_MIN(type), ATLAS_UV_MAX(type)
#define ATLAS_UV_VERTICES(type) UV_VERTICES(ATLAS_UV_MIN(type), ATLAS_UV_MAX(type))
mat4 canvas_transform_get(Canvas* self, vec2 pan, float zoom, OriginType origin);
void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color);
void canvas_bind(Canvas* self);
void canvas_clear(vec4& color);
void canvas_draw(Canvas* self);
void canvas_free(Canvas* self);
void canvas_grid_draw(Canvas* self, GLuint& shader, mat4& transform, ivec2& size, ivec2& offset, vec4& color);
void canvas_init(Canvas* self, const ivec2& size);
void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color);
void canvas_framebuffer_resize_check(Canvas* self);
void canvas_unbind(void);
void canvas_viewport_set(Canvas* self);
void canvas_texture_draw(Canvas* self, GLuint& shader, GLuint& texture, mat4& transform, const float* vertices = CANVAS_TEXTURE_VERTICES,
vec4 tint = COLOR_OPAQUE, vec3 colorOffset = COLOR_OFFSET_NONE);
Canvas();
Canvas(glm::vec2 size);
~Canvas();
bool is_valid();
void framebuffer_set();
void framebuffer_resize_check();
void size_set(glm::vec2 size);
glm::mat4 transform_get(float zoom, glm::vec2 pan);
void axes_render(shader::Shader& shader, float zoom, glm::vec2 pan, glm::vec4 color = glm::vec4(1.0f));
void grid_render(shader::Shader& shader, float zoom, glm::vec2 pan, glm::ivec2 size = glm::ivec2(32, 32),
glm::ivec2 offset = {}, glm::vec4 color = glm::vec4(1.0f));
void texture_render(shader::Shader& shader, GLuint& texture, glm::mat4& transform, glm::vec4 tint = glm::vec4(1.0f),
glm::vec3 colorOffset = {}, float* vertices = (float*)TEXTURE_VERTICES);
void rect_render(shader::Shader& shader, glm::mat4& transform, glm::vec4 color = glm::vec4(1.0f));
void viewport_set();
void clear(glm::vec4& color);
void bind();
void unbind();
void zoom_set(float& zoom, glm::vec2& pan, glm::vec2& focus, float step);
glm::vec2 position_translate(float& zoom, glm::vec2& pan, glm::vec2 position);
};
}

View File

@@ -1,97 +0,0 @@
#include "clipboard.h"
void clipboard_copy(Clipboard* self) {
std::string clipboardText{};
auto clipboard_text_set = [&]() {
if (!SDL_SetClipboardText(clipboardText.c_str()))
log_warning(std::format(CLIPBOARD_TEXT_SET_WARNING, SDL_GetError()));
};
switch (self->type) {
case CLIPBOARD_FRAME: {
if (Anm2Reference* reference = std::get_if<Anm2Reference>(&self->destination)) {
if (Anm2Frame* frame = anm2_frame_from_reference(self->anm2, *reference)) {
anm2_frame_serialize_to_string(frame, reference->itemType, &clipboardText);
clipboard_text_set();
}
}
break;
}
case CLIPBOARD_ANIMATION: {
if (std::set<int>* set = std::get_if<std::set<int>>(&self->source)) {
for (auto& i : *set) {
if (Anm2Animation* animation = anm2_animation_from_reference(self->anm2, {i})) {
std::string animationText{};
anm2_animation_serialize_to_string(animation, &animationText);
clipboardText += animationText;
}
}
clipboard_text_set();
}
break;
}
default:
break;
}
}
void clipboard_cut(Clipboard* self) {
clipboard_copy(self);
switch (self->type) {
case CLIPBOARD_FRAME: {
if (Anm2Reference* reference = std::get_if<Anm2Reference>(&self->destination))
anm2_frame_remove(self->anm2, *reference);
break;
}
case CLIPBOARD_ANIMATION: {
if (std::set<int>* set = std::get_if<std::set<int>>(&self->source))
anm2_animations_remove(self->anm2, *set);
break;
}
default:
break;
}
}
bool clipboard_paste(Clipboard* self) {
auto clipboard_string = [&]() {
char* clipboardText = SDL_GetClipboardText();
std::string clipboardString = std::string(clipboardText);
SDL_free(clipboardText);
return clipboardString;
};
switch (self->type) {
case CLIPBOARD_FRAME: {
if (Anm2Reference* reference = std::get_if<Anm2Reference>(&self->destination)) {
Anm2Frame frame;
if (!anm2_frame_deserialize_from_xml(&frame, clipboard_string()))
return false;
anm2_frame_add(self->anm2, &frame, *reference);
}
break;
}
case CLIPBOARD_ANIMATION: {
if (int* index = std::get_if<int>(&self->destination)) {
std::vector<Anm2Animation> clipboardAnimations;
if (!anm2_animations_deserialize_from_xml(clipboardAnimations, clipboard_string()))
return false;
int useIndex = std::clamp(*index + 1, 0, (int)self->anm2->animations.size());
for (auto& animation : clipboardAnimations)
anm2_animation_add(self->anm2, &animation, useIndex++);
}
break;
}
default:
break;
}
return true;
}
void clipboard_init(Clipboard* self, Anm2* anm2) { self->anm2 = anm2; }
bool clipboard_is_value(void) { return SDL_HasClipboardText(); }

View File

@@ -1,23 +0,0 @@
#pragma once
#include "anm2.h"
#include <variant>
#define CLIPBOARD_TEXT_SET_WARNING "Unable to set clipboard text! ({})"
enum ClipboardType { CLIPBOARD_NONE, CLIPBOARD_FRAME, CLIPBOARD_ANIMATION };
using ClipboardValue = std::variant<std::monostate, Anm2Reference, std::set<int>, int>;
struct Clipboard {
Anm2* anm2 = nullptr;
ClipboardType type;
ClipboardValue source;
ClipboardValue destination;
};
bool clipboard_is_value(void);
void clipboard_copy(Clipboard* self);
void clipboard_cut(Clipboard* self);
bool clipboard_paste(Clipboard* self);
void clipboard_init(Clipboard* self, Anm2* anm2);

View File

@@ -1,70 +1,86 @@
#include "dialog.h"
#ifdef _WIN32
#include <windows.h>
#include <window.h>
#endif
static void _dialog_callback(void* userdata, const char* const* filelist, int filter) {
Dialog* self;
#include <format>
self = (Dialog*)userdata;
namespace anm2ed::dialog
{
if (filelist && filelist[0] && strlen(filelist[0]) > 0) {
self->path = filelist[0];
self->isSelected = true;
self->selectedFilter = filter;
} else {
self->isSelected = false;
self->selectedFilter = INDEX_NONE;
constexpr SDL_DialogFileFilter FILE_FILTER_ANM2[] = {{"Anm2 file", "anm2;xml"}};
constexpr SDL_DialogFileFilter FILE_FILTER_SPRITESHEET[] = {{"PNG image", "png"}};
void callback(void* userData, const char* const* filelist, int filter)
{
auto self = (Dialog*)(userData);
if (filelist && filelist[0] && strlen(filelist[0]) > 0)
{
self->path = filelist[0];
self->selectedFilter = filter;
}
else
self->selectedFilter = -1;
}
}
void dialog_init(Dialog* self, SDL_Window* window) { self->window = window; }
Dialog::Dialog() = default;
void dialog_anm2_open(Dialog* self) {
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr, false);
self->type = DIALOG_ANM2_OPEN;
}
Dialog::Dialog(SDL_Window* window)
{
*this = Dialog();
this->window = window;
}
void dialog_anm2_save(Dialog* self) {
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr);
self->type = DIALOG_ANM2_SAVE;
}
void Dialog::anm2_new()
{
SDL_ShowSaveFileDialog(callback, this, window, FILE_FILTER_ANM2, std::size(FILE_FILTER_ANM2), nullptr);
type = ANM2_NEW;
}
void dialog_spritesheet_add(Dialog* self) {
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
self->type = DIALOG_SPRITESHEET_ADD;
}
void Dialog::anm2_open()
{
SDL_ShowOpenFileDialog(callback, this, window, FILE_FILTER_ANM2, std::size(FILE_FILTER_ANM2), nullptr, false);
type = ANM2_OPEN;
}
void dialog_spritesheet_replace(Dialog* self, int id) {
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
self->replaceID = id;
self->type = DIALOG_SPRITESHEET_REPLACE;
}
void Dialog::anm2_save()
{
SDL_ShowSaveFileDialog(callback, this, window, FILE_FILTER_ANM2, std::size(FILE_FILTER_ANM2), nullptr);
type = ANM2_SAVE;
}
void dialog_render_path_set(Dialog* self, RenderType type) {
SDL_DialogFileFilter filter = DIALOG_RENDER_FILE_FILTERS[type];
void Dialog::spritesheet_open()
{
SDL_ShowOpenFileDialog(callback, this, window, FILE_FILTER_SPRITESHEET, std::size(FILE_FILTER_SPRITESHEET), nullptr,
false);
type = SPRITESHEET_OPEN;
}
if (type == RENDER_PNG)
SDL_ShowOpenFolderDialog(_dialog_callback, self, self->window, nullptr, false);
else
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, &filter, 1, nullptr);
self->type = DIALOG_RENDER_PATH_SET;
}
void Dialog::spritesheet_replace()
{
SDL_ShowOpenFileDialog(callback, this, window, FILE_FILTER_SPRITESHEET, std::size(FILE_FILTER_SPRITESHEET), nullptr,
false);
type = SPRITESHEET_REPLACE;
}
void dialog_ffmpeg_path_set(Dialog* self) {
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_FFMPEG, std::size(DIALOG_FILE_FILTER_FFMPEG), nullptr, false);
self->type = DIALOG_FFMPEG_PATH_SET;
}
void dialog_explorer_open(const std::string& path) {
void Dialog::file_explorer_open(const std::string& path)
{
#ifdef _WIN32
ShellExecuteA(NULL, DIALOG_FILE_EXPLORER_COMMAND, path.c_str(), NULL, NULL, SW_SHOWNORMAL);
ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL);
#else
char command[DIALOG_FILE_EXPLORER_COMMAND_SIZE];
snprintf(command, sizeof(command), DIALOG_FILE_EXPLORER_COMMAND, path.c_str());
system(command);
system(std::format("xdg-open \"{}\" &", path).c_str());
#endif
}
}
void dialog_reset(Dialog* self) { *self = {self->window}; }
void Dialog::reset()
{
*this = Dialog(this->window);
}
bool Dialog::is_selected_file(Type type)
{
return this->type == type && !path.empty();
}
};

View File

@@ -1,56 +1,39 @@
#pragma once
#include "render.h"
#include "window.h"
#include <string>
#define DIALOG_FILE_EXPLORER_COMMAND_SIZE 512
#include <SDL3/SDL.h>
#ifdef _WIN32
#define DIALOG_FILE_EXPLORER_COMMAND "open"
#else
#define DIALOG_FILE_EXPLORER_COMMAND "xdg-open \"%s\" &"
#endif
namespace anm2ed::dialog
{
enum Type
{
NONE,
ANM2_NEW,
ANM2_OPEN,
ANM2_SAVE,
SPRITESHEET_OPEN,
SPRITESHEET_REPLACE
};
const SDL_DialogFileFilter DIALOG_FILE_FILTER_ANM2[] = {{"Anm2 file", "anm2;xml"}};
class Dialog
{
public:
SDL_Window* window{};
std::string path{};
Type type{NONE};
int selectedFilter{-1};
int replaceID{-1};
const SDL_DialogFileFilter DIALOG_FILE_FILTER_PNG[] = {{"PNG image", "png"}};
const SDL_DialogFileFilter DIALOG_RENDER_FILE_FILTERS[] = {{"PNG image", "png"}, {"GIF image", "gif"}, {"WebM video", "webm"}, {"MP4 video", "mp4"}};
const SDL_DialogFileFilter DIALOG_FILE_FILTER_FFMPEG[] = {
#ifdef _WIN32
{"Executable", "exe"}
#else
{"Executable", ""}
#endif
};
enum DialogType {
DIALOG_NONE,
DIALOG_ANM2_OPEN,
DIALOG_ANM2_SAVE,
DIALOG_SPRITESHEET_ADD,
DIALOG_SPRITESHEET_REPLACE,
DIALOG_RENDER_PATH_SET,
DIALOG_FFMPEG_PATH_SET
};
struct Dialog {
SDL_Window* window = nullptr;
std::string path{};
int selectedFilter = ID_NONE;
int replaceID = ID_NONE;
DialogType type = DIALOG_NONE;
bool isSelected{};
};
void dialog_init(Dialog* self, SDL_Window* window);
void dialog_anm2_open(Dialog* self);
void dialog_spritesheet_add(Dialog* self);
void dialog_spritesheet_replace(Dialog* self, int id);
void dialog_anm2_save(Dialog* self);
void dialog_render_path_set(Dialog* self, RenderType type);
void dialog_render_directory_set(Dialog* self);
void dialog_ffmpeg_path_set(Dialog* self);
void dialog_reset(Dialog* self);
void dialog_explorer_open(const std::string& path);
Dialog();
Dialog(SDL_Window* window);
void anm2_new();
void anm2_open();
void anm2_save();
void spritesheet_open();
void spritesheet_replace();
void file_explorer_open(const std::string& path);
void reset();
bool is_selected_file(Type type);
};
}

54
src/dockspace.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "dockspace.h"
#include "animations.h"
#include "onionskin.h"
#include "tools.h"
using namespace anm2ed::animations;
using namespace anm2ed::dialog;
using namespace anm2ed::document_manager;
using namespace anm2ed::documents;
using namespace anm2ed::playback;
using namespace anm2ed::resources;
using namespace anm2ed::settings;
using namespace anm2ed::taskbar;
namespace anm2ed::dockspace
{
void Dockspace::update(Taskbar& taskbar, Documents& documents, DocumentManager& manager, Settings& settings,
Resources& resources, Dialog& dialog, Playback& playback)
{
auto viewport = ImGui::GetMainViewport();
auto windowHeight = viewport->Size.y - taskbar.height - documents.height;
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight));
if (ImGui::Begin("##DockSpace", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus))
{
if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode))
{
if (manager.get())
{
if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources, playback);
if (settings.windowIsAnimations) animations.update(manager, settings, resources);
if (settings.windowIsEvents) events.update(manager, settings, resources);
if (settings.windowIsFrameProperties) frameProperties.update(manager, settings);
if (settings.windowIsLayers) layers.update(manager, settings, resources);
if (settings.windowIsNulls) nulls.update(manager, settings, resources);
if (settings.windowIsOnionskin) onionskin.update(settings);
if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources);
if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog);
if (settings.windowIsTimeline) timeline.update(manager, settings, resources, playback);
if (settings.windowIsTools) tools.update(settings, resources);
}
}
}
ImGui::End();
}
}

38
src/dockspace.h Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#include "animation_preview.h"
#include "animations.h"
#include "documents.h"
#include "events.h"
#include "frame_properties.h"
#include "layers.h"
#include "nulls.h"
#include "onionskin.h"
#include "spritesheet_editor.h"
#include "spritesheets.h"
#include "taskbar.h"
#include "timeline.h"
#include "tools.h"
namespace anm2ed::dockspace
{
class Dockspace
{
animation_preview::AnimationPreview animationPreview;
animations::Animations animations;
events::Events events;
frame_properties::FrameProperties frameProperties;
layers::Layers layers;
nulls::Nulls nulls;
onionskin::Onionskin onionskin;
spritesheet_editor::SpritesheetEditor spritesheetEditor;
spritesheets::Spritesheets spritesheets;
timeline::Timeline timeline;
tools::Tools tools;
public:
void update(taskbar::Taskbar& taskbar, documents::Documents& documents, document_manager::DocumentManager& manager,
settings::Settings& settings, resources::Resources& resources, dialog::Dialog& dialog,
playback::Playback& playback);
};
}

91
src/document.cpp Normal file
View File

@@ -0,0 +1,91 @@
#include "document.h"
#include "anm2.h"
#include "filesystem.h"
using namespace anm2ed::anm2;
using namespace anm2ed::filesystem;
namespace anm2ed::document
{
Document::Document() = default;
Document::Document(const std::string& path, bool isNew, std::string* errorString)
{
if (!path_is_exist(path)) return;
if (isNew)
anm2 = anm2::Anm2();
else
{
anm2 = Anm2(path, errorString);
if (errorString && !errorString->empty()) return;
}
this->path = path;
hash_set();
}
bool Document::save(const std::string& path, std::string* errorString)
{
this->path = !path.empty() ? path : this->path.string();
if (anm2.serialize(this->path, errorString))
{
hash_set();
return true;
}
return false;
}
void Document::hash_set()
{
saveHash = anm2.hash();
hash = saveHash;
}
void Document::hash_time(double time, double interval)
{
if (time - lastHashTime > interval)
{
hash = anm2.hash();
lastHashTime = time;
}
}
bool Document::is_dirty()
{
return hash != saveHash;
}
std::string Document::directory_get()
{
return path.parent_path();
}
std::string Document::filename_get()
{
return path.filename().string();
}
anm2::Animation* Document::animation_get()
{
return anm2.animation_get(reference);
}
anm2::Frame* Document::frame_get()
{
return anm2.frame_get(reference);
}
anm2::Spritesheet* Document::spritesheet_get()
{
return anm2.spritesheet_get(referenceSpritesheet);
}
bool Document::is_valid()
{
return !path.empty();
}
};

51
src/document.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include "anm2.h"
#include <filesystem>
#include <set>
#include <glm/glm.hpp>
namespace anm2ed::document
{
class Document
{
public:
std::filesystem::path path{};
anm2::Anm2 anm2{};
anm2::Reference reference{};
float previewZoom{200};
glm::vec2 previewPan{};
glm::vec2 editorPan{};
float editorZoom{200};
int overlayIndex{};
int referenceSpritesheet{-1};
std::set<int> selectedEvents{};
std::set<int> selectedLayers{};
std::set<int> selectedNulls{};
std::set<int> selectedAnimations{};
std::set<int> selectedSpritesheets{};
uint64_t hash{};
uint64_t saveHash{};
double lastHashTime{};
bool isOpen{true};
Document();
Document(const std::string& path, bool isNew = false, std::string* errorString = nullptr);
bool save(const std::string& path = {}, std::string* errorString = nullptr);
void hash_set();
void hash_time(double time, double interval = 1.0);
bool is_dirty();
std::string directory_get();
std::string filename_get();
anm2::Animation* animation_get();
anm2::Frame* frame_get();
anm2::Spritesheet* spritesheet_get();
bool is_valid();
};
};

76
src/document_manager.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include "document_manager.h"
#include "toast.h"
#include "util.h"
using namespace anm2ed::toast;
using namespace anm2ed::util;
namespace anm2ed::document_manager
{
Document* DocumentManager::get()
{
return vector::find(documents, selected);
}
Document* DocumentManager::get(int index)
{
return vector::find(documents, index);
}
bool DocumentManager::open(const std::string& path, bool isNew)
{
std::string errorString{};
Document document = Document(path, isNew, &errorString);
if (document.is_valid())
{
documents.emplace_back(std::move(document));
selected = documents.size() - 1;
pendingSelected = selected;
toasts.add(std::format("Opened document: {}", path));
return true;
}
toasts.add_error(std::format("Failed to open document: {} ({})", path, errorString));
return false;
}
bool DocumentManager::new_(const std::string& path)
{
return open(path, true);
}
void DocumentManager::save(int index, const std::string& path)
{
auto document = get(index);
if (!document) return;
std::string errorString{};
document->path = !path.empty() ? path : document->path.string();
if (document->save(document->path, &errorString))
toasts.add(std::format("Saved document: {}", document->path.string()));
else
toasts.add_error(std::format("Failed to save document: {} ({})", document->path.string(), errorString));
}
void DocumentManager::save(const std::string& path)
{
save(selected, path);
}
void DocumentManager::close(int index)
{
documents.erase(documents.begin() + index);
}
void DocumentManager::spritesheet_add(const std::string& path)
{
auto document = get();
if (!document) return;
if (int id{}; document->anm2.spritesheet_add(document->directory_get(), path, id))
toasts.add(std::format("Added spritesheet #{}: {}", id, path));
else
toasts.add(std::format("Failed to add spritesheet #{}: {}", id, path));
}
}

27
src/document_manager.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include "document.h"
using namespace anm2ed::document;
namespace anm2ed::document_manager
{
class DocumentManager
{
public:
std::vector<Document> documents{};
int selected{};
int pendingSelected{};
Document* get();
Document* get(int index);
bool open(const std::string& path, bool isNew = false);
bool new_(const std::string& path);
void save(int index, const std::string& path = {});
void save(const std::string& path = {});
void close(int index);
void spritesheet_add(const std::string& path);
};
}

129
src/documents.cpp Normal file
View File

@@ -0,0 +1,129 @@
#include "documents.h"
#include <ranges>
#include "imgui.h"
using namespace anm2ed::taskbar;
using namespace anm2ed::document_manager;
using namespace anm2ed::resources;
namespace anm2ed::documents
{
void Documents::update(Taskbar& taskbar, DocumentManager& manager, Resources& resources)
{
auto viewport = ImGui::GetMainViewport();
auto windowHeight = ImGui::GetFrameHeightWithSpacing();
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight));
if (ImGui::Begin("##Documents", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse))
{
height = ImGui::GetWindowSize().y;
if (ImGui::BeginTabBar("Documents Bar", ImGuiTabBarFlags_Reorderable))
{
for (auto [i, document] : std::views::enumerate(manager.documents))
{
auto isDirty = document.is_dirty();
auto isRequested = i == manager.pendingSelected;
auto isSelected = i == manager.selected;
if (isSelected) document.hash_time(ImGui::GetTime());
auto font = isDirty ? font::ITALICS : font::REGULAR;
auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get()) : document.filename_get();
auto label = std::format("{}###Document{}", string, i);
auto flags = isDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
if (isRequested) flags |= ImGuiTabItemFlags_SetSelected;
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags))
{
manager.selected = i;
if (isRequested) manager.pendingSelected = -1;
ImGui::EndTabItem();
}
ImGui::PopFont();
if (!document.isOpen)
{
if (isDirty)
{
isCloseDocument = true;
isOpenCloseDocumentPopup = true;
closeDocumentIndex = i;
document.isOpen = true;
}
else
manager.close(i);
}
}
ImGui::EndTabBar();
}
if (isOpenCloseDocumentPopup)
{
ImGui::OpenPopup("Close Document");
isOpenCloseDocumentPopup = false;
}
if (isCloseDocument)
{
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(), ImGuiCond_None);
if (ImGui::BeginPopupModal("Close Document", nullptr, ImGuiWindowFlags_NoResize))
{
auto closeDocument = manager.get(closeDocumentIndex);
ImGui::TextUnformatted(std::format("The document \"{}\" has been modified.\nDo you want to save it?",
closeDocument->filename_get())
.c_str());
auto widgetSize = imgui::widget_size_with_row_get(3);
auto close = [&]()
{
closeDocumentIndex = 0;
isCloseDocument = false;
ImGui::CloseCurrentPopup();
};
if (ImGui::Button("Yes", widgetSize))
{
manager.save(closeDocumentIndex);
manager.close(closeDocumentIndex);
close();
}
ImGui::SameLine();
if (ImGui::Button("No", widgetSize))
{
manager.close(closeDocumentIndex);
close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) close();
ImGui::EndPopup();
}
}
}
ImGui::End();
}
}

20
src/documents.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "document_manager.h"
#include "resources.h"
#include "taskbar.h"
namespace anm2ed::documents
{
class Documents
{
bool isCloseDocument{};
bool isOpenCloseDocumentPopup{};
int closeDocumentIndex{};
public:
float height{};
void update(taskbar::Taskbar& taskbar, document_manager::DocumentManager& manager, resources::Resources& resources);
};
}

View File

@@ -1,54 +0,0 @@
#include "editor.h"
void editor_init(Editor* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings) {
self->anm2 = anm2;
self->reference = reference;
self->resources = resources;
self->settings = settings;
canvas_init(&self->canvas, vec2());
}
void editor_draw(Editor* self) {
ivec2& gridSize = self->settings->editorGridSize;
ivec2& gridOffset = self->settings->editorGridOffset;
vec4& gridColor = self->settings->editorGridColor;
GLuint& shaderLine = self->resources->shaders[SHADER_LINE];
GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE];
GLuint& shaderGrid = self->resources->shaders[SHADER_GRID];
mat4 transform = canvas_transform_get(&self->canvas, self->settings->editorPan, self->settings->editorZoom, ORIGIN_TOP_LEFT);
canvas_framebuffer_resize_check(&self->canvas);
canvas_bind(&self->canvas);
canvas_viewport_set(&self->canvas);
canvas_clear(self->settings->editorBackgroundColor);
if (Anm2Spritesheet* spritesheet = map_find(self->anm2->spritesheets, self->spritesheetID)) {
Texture& texture = spritesheet->texture;
mat4 spritesheetTransform = transform * quad_model_get(texture.size);
canvas_texture_draw(&self->canvas, shaderTexture, texture.id, spritesheetTransform);
if (self->settings->editorIsBorder)
canvas_rect_draw(&self->canvas, shaderLine, spritesheetTransform, EDITOR_BORDER_COLOR);
Anm2Frame* frame = (Anm2Frame*)anm2_frame_from_reference(self->anm2, *self->reference);
if (frame) {
mat4 cropTransform = transform * quad_model_get(frame->size, frame->crop);
canvas_rect_draw(&self->canvas, shaderLine, cropTransform, EDITOR_FRAME_COLOR);
mat4 pivotTransform = transform * quad_model_get(CANVAS_PIVOT_SIZE, frame->crop + frame->pivot, CANVAS_PIVOT_SIZE * 0.5f);
float vertices[] = ATLAS_UV_VERTICES(ATLAS_PIVOT);
canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, pivotTransform, vertices, EDITOR_PIVOT_COLOR);
}
}
if (self->settings->editorIsGrid)
canvas_grid_draw(&self->canvas, shaderGrid, transform, gridSize, gridOffset, gridColor);
canvas_unbind();
}
void editor_free(Editor* self) { canvas_free(&self->canvas); }

View File

@@ -1,41 +0,0 @@
#pragma once
#include "anm2.h"
#include "canvas.h"
#include "resources.h"
#include "settings.h"
#define EDITOR_ZOOM_MIN 1.0f
#define EDITOR_ZOOM_MAX 1000.0f
#define EDITOR_ZOOM_STEP 25.0
#define EDITOR_GRID_MIN 1
#define EDITOR_GRID_MAX 1000
#define EDITOR_GRID_OFFSET_MIN 0
#define EDITOR_GRID_OFFSET_MAX 100
static const vec4 EDITOR_BORDER_COLOR = COLOR_OPAQUE;
static const vec4 EDITOR_FRAME_COLOR = COLOR_RED;
static const vec4 EDITOR_PIVOT_COLOR = COLOR_PINK;
struct Editor {
Anm2* anm2 = nullptr;
Anm2Reference* reference = nullptr;
Resources* resources = nullptr;
Settings* settings = nullptr;
Canvas canvas;
GLuint fbo;
GLuint rbo;
GLuint gridVAO;
GLuint gridVBO;
GLuint texture;
GLuint textureEBO;
GLuint textureVAO;
GLuint textureVBO;
GLuint borderVAO;
GLuint borderVBO;
int spritesheetID = ID_NONE;
};
void editor_init(Editor* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings);
void editor_draw(Editor* self);
void editor_free(Editor* self);

83
src/events.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "events.h"
#include <ranges>
#include "imgui.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::events
{
void Events::update(DocumentManager& manager, Settings& settings, Resources& resources)
{
if (ImGui::Begin("Events", &settings.windowIsEvents))
{
auto document = manager.get();
anm2::Anm2& anm2 = document->anm2;
auto& selection = document->selectedEvents;
storage.UserData = &selection;
storage.AdapterSetItemSelected = imgui::external_storage_set;
auto childSize = imgui::size_with_footer_get();
if (ImGui::BeginChild("##Events Child", childSize, true))
{
ImGuiMultiSelectIO* io =
ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), anm2.content.events.size());
storage.ApplyRequests(io);
for (auto& [id, event] : anm2.content.events)
{
auto isSelected = selection.contains(id);
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
imgui::selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id),
event.name, isSelected);
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(event.name.c_str());
ImGui::PopFont();
ImGui::EndTooltip();
}
ImGui::PopID();
}
io = ImGui::EndMultiSelect();
storage.ApplyRequests(io);
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd, true);
if (ImGui::Button("Add", widgetSize))
{
int id{};
anm2.event_add(id);
selection = {id};
}
imgui::set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd);
ImGui::SameLine();
std::set<int> unusedEventIDs = anm2.events_unused();
imgui::shortcut(settings.shortcutRemove, true);
ImGui::BeginDisabled(unusedEventIDs.empty());
{
if (ImGui::Button("Remove Unused", widgetSize))
for (auto& id : unusedEventIDs)
anm2.content.layers.erase(id);
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
}
}

17
src/events.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "document_manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::events
{
class Events
{
ImGuiSelectionExternalStorage storage{};
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
};
}

View File

@@ -1,56 +0,0 @@
#include "ffmpeg.h"
bool ffmpeg_render(const std::string& ffmpegPath, const std::string& outputPath, const std::vector<Texture>& frames, ivec2 size, int fps,
enum RenderType type) {
if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || outputPath.empty())
return false;
std::string command{};
switch (type) {
case RENDER_GIF:
command = std::format(FFMPEG_GIF_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
case RENDER_WEBM:
command = std::format(FFMPEG_WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
case RENDER_MP4:
command = std::format(FFMPEG_MP4_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
default:
break;
}
#if _WIN32
command = string_quote(command);
#endif
log_command(command);
FILE* fp = POPEN(command.c_str(), PWRITE_MODE);
if (!fp) {
log_error(std::format(FFMPEG_POPEN_ERROR, strerror(errno)));
return false;
}
size_t frameBytes = size.x * size.y * TEXTURE_CHANNELS;
for (const auto& frame : frames) {
std::vector<u8> rgba = texture_download(&frame);
if (rgba.size() != frameBytes) {
PCLOSE(fp);
return false;
}
if (fwrite(rgba.data(), 1, frameBytes, fp) != frameBytes) {
PCLOSE(fp);
return false;
}
}
const int code = PCLOSE(fp);
return (code == 0);
}

View File

@@ -1,26 +0,0 @@
#pragma once
#include "render.h"
#include "texture.h"
#define FFMPEG_POPEN_ERROR "popen() (for FFmpeg) failed!\n{}"
static constexpr const char* FFMPEG_GIF_FORMAT = "\"{0}\" -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-lavfi \"split[s0][s1];"
"[s0]palettegen=stats_mode=full[p];"
"[s1][p]paletteuse=dither=floyd_steinberg\" "
"-loop 0 \"{4}\"";
static constexpr const char* FFMPEG_WEBM_FORMAT = "\"{0}\" -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 "
"-auto-alt-ref 0 -an \"{4}\"";
static constexpr const char* FFMPEG_MP4_FORMAT = "\"{0}\" -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" "
"-c:v libx265 -crf 20 -preset slow "
"-tag:v hvc1 -movflags +faststart -an \"{4}\"";
bool ffmpeg_render(const std::string& ffmpegPath, const std::string& outputPath, const std::vector<Texture>& frames, ivec2 size, int fps, enum RenderType type);

47
src/filesystem.cpp Normal file
View File

@@ -0,0 +1,47 @@
#include "filesystem.h"
#include <SDL3/SDL_filesystem.h>
#include <SDL3/SDL_stdinc.h>
#include <filesystem>
namespace anm2ed::filesystem
{
std::string path_preferences_get()
{
char* preferencesPath = SDL_GetPrefPath("", "anm2ed");
std::string preferencesPathString = preferencesPath;
SDL_free(preferencesPath);
return preferencesPathString;
}
bool path_is_exist(const std::string& path)
{
std::error_code errorCode;
return std::filesystem::exists(path, errorCode) && ((void)std::filesystem::status(path, errorCode), !errorCode);
}
bool path_is_extension(const std::string& path, const std::string& extension)
{
auto e = std::filesystem::path(path).extension().string();
std::transform(e.begin(), e.end(), e.begin(), ::tolower);
return e == ("." + extension);
}
WorkingDirectory::WorkingDirectory(const std::string& path, bool isFile)
{
previous = std::filesystem::current_path();
if (isFile)
{
std::filesystem::path filePath = path;
std::filesystem::path parentPath = filePath.parent_path();
std::filesystem::current_path(parentPath);
}
else
std::filesystem::current_path(path);
}
WorkingDirectory::~WorkingDirectory()
{
std::filesystem::current_path(previous);
}
}

20
src/filesystem.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <filesystem>
#include <string>
namespace anm2ed::filesystem
{
std::string path_preferences_get();
bool path_is_exist(const std::string& path);
bool path_is_extension(const std::string& path, const std::string& extension);
class WorkingDirectory
{
public:
std::filesystem::path previous;
WorkingDirectory(const std::string& path, bool isFile = false);
~WorkingDirectory();
};
}

33
src/font.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "font.h"
namespace anm2ed::font
{
Font::Font() = default;
Font::Font(void* data, size_t length, int size)
{
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
pointer = ImGui::GetIO().Fonts->AddFontFromMemoryTTF(data, length, size, &config);
}
Font::~Font()
{
if (get()) ImGui::GetIO().Fonts->RemoveFont(pointer);
}
ImFont* Font::get()
{
return pointer;
}
Font& Font::operator=(Font&& other) noexcept
{
if (this != &other)
{
pointer = other.pointer;
other.pointer = nullptr;
}
return *this;
}
}

5238
src/font.h Normal file

File diff suppressed because it is too large Load Diff

124
src/frame_properties.cpp Normal file
View File

@@ -0,0 +1,124 @@
#include "frame_properties.h"
#include <ranges>
#include <glm/gtc/type_ptr.hpp>
#include "imgui.h"
#include "math.h"
#include "types.h"
using namespace anm2ed::settings;
using namespace anm2ed::document_manager;
using namespace anm2ed::math;
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::frame_properties
{
void FrameProperties::update(DocumentManager& manager, Settings& settings)
{
if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties))
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto& type = reference.itemType;
auto& isRound = settings.propertiesIsRound;
auto frame = document.frame_get();
ImGui::BeginDisabled(!frame);
{
if (type == anm2::TRIGGER)
{
std::vector<std::string> eventNames{};
for (auto& event : anm2.content.events | std::views::values)
eventNames.emplace_back(event.name);
imgui::combo_strings("Event", frame ? &frame->eventID : &dummy_value<int>(), eventNames);
ImGui::SetItemTooltip("%s", "Change the event this trigger uses.");
ImGui::InputInt("At Frame", frame ? &frame->atFrame : &dummy_value<int>(), step::NORMAL, step::FAST,
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0);
ImGui::SetItemTooltip("%s", "Change the frame the trigger will be activated at.");
}
else
{
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
{
if (ImGui::InputFloat2("Crop", frame ? value_ptr(frame->crop) : &dummy_value<float>(),
frame ? vec2_format_get(frame->crop) : ""))
if (isRound) frame->crop = ivec2(frame->crop);
ImGui::SetItemTooltip("%s", "Change the crop position the frame uses.");
if (ImGui::InputFloat2("Size", frame ? value_ptr(frame->size) : &dummy_value<float>(),
frame ? vec2_format_get(frame->size) : ""))
if (isRound) frame->crop = ivec2(frame->size);
ImGui::SetItemTooltip("%s", "Change the size of the crop the frame uses.");
}
ImGui::EndDisabled();
if (ImGui::InputFloat2("Position", frame ? value_ptr(frame->position) : &dummy_value<float>(),
frame ? vec2_format_get(frame->position) : ""))
if (isRound) frame->position = ivec2(frame->position);
ImGui::SetItemTooltip("%s", "Change the position of the frame.");
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_);
{
if (ImGui::InputFloat2("Pivot", frame ? value_ptr(frame->pivot) : &dummy_value<float>(),
frame ? vec2_format_get(frame->pivot) : ""))
if (isRound) frame->position = ivec2(frame->position);
ImGui::SetItemTooltip("%s", "Change the pivot of the frame; i.e., where it is centered.");
}
ImGui::EndDisabled();
if (ImGui::InputFloat2("Scale", frame ? value_ptr(frame->scale) : &dummy_value<float>(),
frame ? vec2_format_get(frame->scale) : ""))
if (isRound) frame->position = ivec2(frame->position);
ImGui::SetItemTooltip("%s", "Change the scale of the frame, in percent.");
if (ImGui::InputFloat("Rotation", frame ? &frame->rotation : &dummy_value<float>(), step::NORMAL, step::FAST,
frame ? float_format_get(frame->rotation) : ""))
if (isRound) frame->rotation = (int)frame->rotation;
ImGui::SetItemTooltip("%s", "Change the rotation of the frame.");
ImGui::InputInt("Duration", frame ? &frame->delay : &dummy_value<int>(), step::NORMAL, step::FAST,
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0);
ImGui::SetItemTooltip("%s", "Change how long the frame lasts.");
ImGui::ColorEdit4("Tint", frame ? value_ptr(frame->tint) : &dummy_value<float>());
ImGui::SetItemTooltip("%s", "Change the tint of the frame.");
ImGui::ColorEdit3("Color Offset", frame ? value_ptr(frame->offset) : &dummy_value<float>());
ImGui::SetItemTooltip("%s", "Change the color added onto the frame.");
ImGui::Checkbox("Visible", frame ? &frame->isVisible : &dummy_value<bool>());
ImGui::SetItemTooltip("%s", "Toggle the frame's visibility.");
ImGui::SameLine();
ImGui::Checkbox("Interpolated", frame ? &frame->isInterpolated : &dummy_value<bool>());
ImGui::SetItemTooltip(
"%s", "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time.");
ImGui::SameLine();
ImGui::Checkbox("Round", &settings.propertiesIsRound);
ImGui::SetItemTooltip(
"%s", "When toggled, decimal values will be snapped to their nearest whole value when changed.");
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button("Flip X", widgetSize))
if (frame) frame->scale.x = -frame->scale.x;
ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame "
"horizontally.\n(Note: the format does not support mirroring.)");
ImGui::SameLine();
if (ImGui::Button("Flip Y", widgetSize))
if (frame) frame->scale.y = -frame->scale.y;
ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame "
"vertically.\n(Note: the format does not support mirroring.)");
}
}
ImGui::EndDisabled();
}
ImGui::End();
}
}

13
src/frame_properties.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include "document_manager.h"
#include "settings.h"
namespace anm2ed::frame_properties
{
class FrameProperties
{
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings);
};
}

View File

@@ -1,50 +0,0 @@
#include "generate_preview.h"
void generate_preview_init(GeneratePreview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings) {
self->anm2 = anm2;
self->reference = reference;
self->resources = resources;
self->settings = settings;
canvas_init(&self->canvas, GENERATE_PREVIEW_SIZE);
}
void generate_preview_draw(GeneratePreview* self) {
static auto& columns = self->settings->generateColumns;
static auto& count = self->settings->generateCount;
static GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE];
const mat4 transform = canvas_transform_get(&self->canvas, {}, CANVAS_ZOOM_DEFAULT, ORIGIN_CENTER);
vec2 startPosition = {self->settings->generateStartPosition.x, self->settings->generateStartPosition.y};
vec2 size = {self->settings->generateSize.x, self->settings->generateSize.y};
vec2 pivot = {self->settings->generatePivot.x, self->settings->generatePivot.y};
canvas_bind(&self->canvas);
canvas_viewport_set(&self->canvas);
canvas_clear(self->settings->previewBackgroundColor);
Anm2Item* item = anm2_item_from_reference(self->anm2, *self->reference);
if (item) {
if (Anm2Spritesheet* spritesheet = map_find(self->anm2->spritesheets, self->anm2->layers[self->reference->itemID].spritesheetID)) {
Texture& texture = spritesheet->texture;
const int index = std::clamp((int)(self->time * count), 0, count);
const int row = index / columns;
const int column = index % columns;
vec2 crop = startPosition + vec2(size.x * column, size.y * row);
vec2 textureSize = vec2(texture.size);
vec2 uvMin = (crop + vec2(0.5f)) / textureSize;
vec2 uvMax = (crop + size - vec2(0.5f)) / textureSize;
float vertices[] = UV_VERTICES(uvMin, uvMax);
mat4 generateTransform = transform * quad_model_get(size, {}, pivot);
canvas_texture_draw(&self->canvas, shaderTexture, texture.id, generateTransform, vertices, COLOR_OPAQUE, COLOR_OFFSET_NONE);
}
}
canvas_unbind();
}
void generate_preview_free(GeneratePreview* self) { canvas_free(&self->canvas); }

View File

@@ -1,24 +0,0 @@
#pragma once
#include "anm2.h"
#include "canvas.h"
#include "resources.h"
#include "settings.h"
#define GENERATE_PREVIEW_TIME_MIN 0.0f
#define GENERATE_PREVIEW_TIME_MAX 1.0f
const vec2 GENERATE_PREVIEW_SIZE = {325, 215};
struct GeneratePreview {
Anm2* anm2 = nullptr;
Anm2Reference* reference = nullptr;
Resources* resources = nullptr;
Settings* settings = nullptr;
Canvas canvas;
float time{};
};
void generate_preview_init(GeneratePreview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings);
void generate_preview_draw(GeneratePreview* self);
void generate_preview_free(GeneratePreview* self);

200
src/icon.h Normal file
View File

@@ -0,0 +1,200 @@
#pragma once
#include <cstring>
#include <glm/ext/vector_int2.hpp>
namespace icon
{
constexpr auto SIZE_SMALL = glm::ivec2(64, 64);
constexpr auto SIZE_NORMAL = glm::ivec2(128, 128);
constexpr auto SIZE_LARGE = glm::ivec2(256, 256);
constexpr auto SIZE_HUGE = glm::ivec2(512, 512);
constexpr auto NONE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM11 15V17H13V15H11ZM11 7V13H13V7H11Z"/></svg>
)";
constexpr auto FILE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M21 9V20.9925C21 21.5511 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.44694 2 3.99826 2H14V8C14 8.55228 14.4477 9 15 9H21ZM21 7H16V2.00318L21 7Z"/></svg>
)";
constexpr auto FOLDER_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M3 3C2.44772 3 2 3.44772 2 4V7H9.58579L12 4.58579L10.4142 3H3ZM14.4142 5L10.4142 9H2V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V6C22 5.44772 21.5523 5 21 5H14.4142Z"/></svg>
)";
constexpr auto CLOSE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"/></svg>
)";
constexpr auto ROOT_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M15.382 4H22V6H16.618L9 21.2361L5.38197 14H2V12H6.61803L9 16.7639L15.382 4Z"/></svg>
)";
constexpr auto LAYER_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M20.0833 10.4999L21.2854 11.2212C21.5221 11.3633 21.5989 11.6704 21.4569 11.9072C21.4146 11.9776 21.3557 12.0365 21.2854 12.0787L11.9999 17.6499L2.71451 12.0787C2.47772 11.9366 2.40093 11.6295 2.54301 11.3927C2.58523 11.3223 2.64413 11.2634 2.71451 11.2212L3.9166 10.4999L11.9999 15.3499L20.0833 10.4999ZM20.0833 15.1999L21.2854 15.9212C21.5221 16.0633 21.5989 16.3704 21.4569 16.6072C21.4146 16.6776 21.3557 16.7365 21.2854 16.7787L12.5144 22.0412C12.1977 22.2313 11.8021 22.2313 11.4854 22.0412L2.71451 16.7787C2.47772 16.6366 2.40093 16.3295 2.54301 16.0927C2.58523 16.0223 2.64413 15.9634 2.71451 15.9212L3.9166 15.1999L11.9999 20.0499L20.0833 15.1999ZM12.5144 1.30864L21.2854 6.5712C21.5221 6.71327 21.5989 7.0204 21.4569 7.25719C21.4146 7.32757 21.3557 7.38647 21.2854 7.42869L11.9999 12.9999L2.71451 7.42869C2.47772 7.28662 2.40093 6.97949 2.54301 6.7427C2.58523 6.67232 2.64413 6.61343 2.71451 6.5712L11.4854 1.30864C11.8021 1.11864 12.1977 1.11864 12.5144 1.30864Z"/></svg>
)";
constexpr auto NULL_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5ZM3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V9C7 10.1046 7.89543 11 9 11H15C16.1046 11 17 10.1046 17 9V8.82929C15.8348 8.41746 15 7.30622 15 6C15 4.34315 16.3431 3 18 3C19.6569 3 21 4.34315 21 6C21 7.30622 20.1652 8.41746 19 8.82929V9C19 11.2091 17.2091 13 15 13H13V15.1707C14.1652 15.5825 15 16.6938 15 18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18C9 16.6938 9.83481 15.5825 11 15.1707V13H9C6.79086 13 5 11.2091 5 9V8.82929C3.83481 8.41746 3 7.30622 3 6ZM18 5C17.4477 5 17 5.44772 17 6C17 6.55228 17.4477 7 18 7C18.5523 7 19 6.55228 19 6C19 5.44772 18.5523 5 18 5ZM12 17C11.4477 17 11 17.4477 11 18C11 18.5523 11.4477 19 12 19C12.5523 19 13 18.5523 13 18C13 17.4477 12.5523 17 12 17Z"/></svg>
)";
constexpr auto TRIGGERS_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M3 3H12.382C12.7607 3 13.107 3.214 13.2764 3.55279L14 5H20C20.5523 5 21 5.44772 21 6V17C21 17.5523 20.5523 18 20 18H13.618C13.2393 18 12.893 17.786 12.7236 17.4472L12 16H5V22H3V3Z"/></svg>
)";
constexpr auto VISIBLE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12ZM12.0003 17C14.7617 17 17.0003 14.7614 17.0003 12C17.0003 9.23858 14.7617 7 12.0003 7C9.23884 7 7.00026 9.23858 7.00026 12C7.00026 14.7614 9.23884 17 12.0003 17ZM12.0003 15C10.3434 15 9.00026 13.6569 9.00026 12C9.00026 10.3431 10.3434 9 12.0003 9C13.6571 9 15.0003 10.3431 15.0003 12C15.0003 13.6569 13.6571 15 12.0003 15Z"/></svg>
)";
constexpr auto INVISIBLE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457ZM14.7577 16.1718L13.2937 14.7078C12.902 14.8952 12.4634 15.0002 12.0003 15.0002C10.3434 15.0002 9.00026 13.657 9.00026 12.0002C9.00026 11.537 9.10522 11.0984 9.29263 10.7067L7.82866 9.24277C7.30514 10.0332 7.00026 10.9811 7.00026 12.0002C7.00026 14.7616 9.23884 17.0002 12.0003 17.0002C13.0193 17.0002 13.9672 16.6953 14.7577 16.1718ZM7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925L16.947 12.7327C16.9821 12.4936 17.0003 12.249 17.0003 12.0002C17.0003 9.23873 14.7617 7.00016 12.0003 7.00016C11.7514 7.00016 11.5068 7.01833 11.2677 7.05343L7.97446 3.76015Z"/></svg>
)";
constexpr auto SHOW_UNUSED_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M6.92893 0.514648L21.0711 14.6568L19.6569 16.071L15.834 12.2486L15 13.4999V21.9999H9V13.4999L4 5.99993H3V3.99993L7.585 3.99965L5.51472 1.92886L6.92893 0.514648ZM9.585 5.99965L6.4037 5.99993L11 12.8944V19.9999H13V12.8944L14.392 10.8066L9.585 5.99965ZM21 3.99993V5.99993H20L18.085 8.87193L16.643 7.42893L17.5963 5.99993H15.213L13.213 3.99993H21Z"/></svg>
)";
constexpr auto HIDE_UNUSED_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M21 4V6H20L15 13.5V22H9V13.5L4 6H3V4H21ZM6.4037 6L11 12.8944V20H13V12.8944L17.5963 6H6.4037Z"/></svg>
)";
constexpr auto SHOW_RECT_DATA = R"(
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect x="4" y="4" width="16" height="16" rx="0.5" stroke="#FFF" stroke-width="2" stroke-linejoin="round"/> <path d="M12 9.5v5M9.5 12h5" stroke="#FFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
)";
constexpr auto HIDE_RECT_DATA = R"(
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect x="4" y="4" width="16" height="16" rx="0.5" stroke="#FFF" stroke-width="2" stroke-linejoin="round"/> <path d="M12 9.5v5M9.5 12h5" stroke="#FFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 2L22 22" stroke="#FFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
)";
constexpr auto ANIMATION_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M5.99807 7L8.30747 3H11.9981L9.68867 7H5.99807ZM11.9981 7L14.3075 3H17.9981L15.6887 7H11.9981ZM17.9981 7L20.3075 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3H5.99807L4 6.46076V19H20V7H17.9981Z"/></svg>
)";
constexpr auto SPRITESHEET_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z"/></svg>
)";
constexpr auto EVENT_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M13 10H20L11 23V14H4L13 1V10Z"/></svg>
)";
constexpr auto PAN_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M12 22L8 18H16L12 22ZM12 2L16 6H8L12 2ZM12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14ZM2 12L6 8V16L2 12ZM22 12L18 16V8L22 12Z"/></svg>
)";
constexpr auto MOVE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M18 11V8L22 12L18 16V13H13V18H16L12 22L8 18H11V13H6V16L2 12L6 8V11H11V6H8L12 2L16 6H13V11H18Z"/></svg>
)";
constexpr auto ROTATE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M16 7H11C7.68629 7 5 9.68629 5 13C5 16.3137 7.68629 19 11 19H20V21H11C6.58172 21 3 17.4183 3 13C3 8.58172 6.58172 5 11 5H16V1L22 6L16 11V7Z"/></svg>
)";
constexpr auto SCALE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M21 3H13.5L16.5429 6.04289L13.2929 9.29289L14.7071 10.7071L17.9571 7.45711L21 10.5V3ZM3 21H10.5L7.45711 17.9571L10.7071 14.7071L9.29289 13.2929L6.04289 16.5429L3 13.5V21Z"/></svg>
)";
constexpr auto CROP_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M15 17V19H6C5.44772 19 5 18.5523 5 18V7H2V5H5V2H7V17H15ZM17 22V7H9V5H18C18.5523 5 19 5.44772 19 6V17H22V19H19V22H17Z"/></svg>
)";
constexpr auto DRAW_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M12.8995 6.85453L17.1421 11.0972L7.24264 20.9967H3V16.754L12.8995 6.85453ZM14.3137 5.44032L16.435 3.319C16.8256 2.92848 17.4587 2.92848 17.8492 3.319L20.6777 6.14743C21.0682 6.53795 21.0682 7.17112 20.6777 7.56164L18.5563 9.68296L14.3137 5.44032Z"/></svg>
)";
constexpr auto ERASE_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M13.9999 18.9966H20.9999V20.9966H11.9999L8.00229 20.9991L1.51457 14.5113C1.12405 14.1208 1.12405 13.4877 1.51457 13.0971L12.1212 2.49053C12.5117 2.1 13.1449 2.1 13.5354 2.49053L21.3136 10.2687C21.7041 10.6592 21.7041 11.2924 21.3136 11.6829L13.9999 18.9966ZM15.6567 14.5113L19.1922 10.9758L12.8283 4.61185L9.29275 8.14738L15.6567 14.5113Z"/></svg>
)";
constexpr auto COLOR_PICKER_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M15.5355 2.80744C17.0976 1.24534 19.6303 1.24534 21.1924 2.80744C22.7545 4.36953 22.7545 6.90219 21.1924 8.46429L18.3638 11.2929L18.7175 11.6466C19.108 12.0371 19.108 12.6703 18.7175 13.0608C18.327 13.4513 17.6938 13.4513 17.3033 13.0608L16.9498 12.7073L10.7351 18.922C10.1767 19.4804 9.46547 19.861 8.6911 20.0159L6.93694 20.3667C6.54976 20.4442 6.19416 20.6345 5.91496 20.9137L4.92894 21.8997C4.53841 22.2902 3.90525 22.2902 3.51472 21.8997L2.10051 20.4855C1.70999 20.095 1.70999 19.4618 2.10051 19.0713L3.08653 18.0852C3.36574 17.806 3.55605 17.4504 3.63348 17.0633L3.98431 15.3091C4.13919 14.5347 4.51981 13.8235 5.07821 13.2651L11.2929 7.05045L10.9393 6.69686C10.5488 6.30634 10.5488 5.67317 10.9393 5.28265C11.3299 4.89212 11.963 4.89212 12.3535 5.28265L12.7069 5.63604L15.5355 2.80744ZM12.7071 8.46466L6.49242 14.6794C6.21322 14.9586 6.02291 15.3142 5.94548 15.7013L5.59464 17.4555C5.43977 18.2299 5.05915 18.9411 4.50075 19.4995C5.05915 18.9411 5.77035 18.5604 6.54471 18.4056L8.29887 18.0547C8.68605 17.9773 9.04165 17.787 9.32085 17.5078L15.5355 11.2931L12.7071 8.46466Z"/></svg>
)";
constexpr auto UNDO_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M19.0001 13.9999L19.0002 5.00003L17.0002 5L17.0001 11.9999L9.41406 12V6.58581L2.99986 13L9.41406 19.4142L9.41406 14L19.0001 13.9999Z"/></svg>
)";
constexpr auto REDO_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M4.99989 13.9999L4.99976 5.00003L6.99976 5L6.99986 11.9999L14.5859 12V6.58581L21.0001 13L14.5859 19.4142L14.5859 14L4.99989 13.9999Z"/></svg>
)";
constexpr auto TARGET_DATA = R"(
<svg viewBox="0 0 24 24" fill="none" stroke="#FFF" stroke-width="1" xmlns="http://www.w3.org/2000/svg"> <circle cx="12" cy="12" r="3.5"/> <line x1="12" y1="-2" x2="12" y2="26"/> <line x1="-2" y1="12" x2="26" y2="12"/> </svg>
)";
constexpr auto INTERPOLATED_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"> <circle cx="12" cy="12" r="2.5"/> </svg>
)";
constexpr auto UNINTERPOLATED_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"> <rect x="9.5" y="9.5" width="5" height="5"/> </svg>
)";
constexpr auto PIVOT_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"/></svg>
)";
constexpr auto TRIGGER_DATA = R"(
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"> <path d="M11 5h2v10h-2V5Zm0 14h2v2h-2v-2Z"/> </svg>
)";
constexpr auto PLAYHEAD_DATA = R"(
<svg viewBox="0 0 24 48" fill="#FFF" xmlns="http://www.w3.org/2000/svg"> <path d="M4 0H20V38L12 48L4 38V0Z"/> </svg>
)";
#define LIST \
X(NONE, NONE_DATA, SIZE_NORMAL) \
X(FILE, FILE_DATA, SIZE_NORMAL) \
X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \
X(CLOSE, CLOSE_DATA, SIZE_NORMAL) \
X(ROOT, ROOT_DATA, SIZE_NORMAL) \
X(LAYER, LAYER_DATA, SIZE_NORMAL) \
X(NULL_, NULL_DATA, SIZE_NORMAL) \
X(TRIGGERS, TRIGGERS_DATA, SIZE_NORMAL) \
X(VISIBLE, VISIBLE_DATA, SIZE_NORMAL) \
X(INVISIBLE, INVISIBLE_DATA, SIZE_NORMAL) \
X(SHOW_RECT, SHOW_RECT_DATA, SIZE_NORMAL) \
X(HIDE_RECT, HIDE_RECT_DATA, SIZE_NORMAL) \
X(SHOW_UNUSED, SHOW_UNUSED_DATA, SIZE_NORMAL) \
X(HIDE_UNUSED, HIDE_UNUSED_DATA, SIZE_NORMAL) \
X(PAN, PAN_DATA, SIZE_NORMAL) \
X(MOVE, MOVE_DATA, SIZE_NORMAL) \
X(ROTATE, ROTATE_DATA, SIZE_NORMAL) \
X(SCALE, SCALE_DATA, SIZE_NORMAL) \
X(CROP, CROP_DATA, SIZE_NORMAL) \
X(DRAW, DRAW_DATA, SIZE_NORMAL) \
X(ERASE, ERASE_DATA, SIZE_NORMAL) \
X(COLOR_PICKER, COLOR_PICKER_DATA, SIZE_NORMAL) \
X(UNDO, UNDO_DATA, SIZE_NORMAL) \
X(REDO, REDO_DATA, SIZE_NORMAL) \
X(ANIMATION, ANIMATION_DATA, SIZE_NORMAL) \
X(SPRITESHEET, SPRITESHEET_DATA, SIZE_NORMAL) \
X(EVENT, EVENT_DATA, SIZE_NORMAL) \
X(INTERPOLATED, INTERPOLATED_DATA, SIZE_NORMAL) \
X(UNINTERPOLATED, UNINTERPOLATED_DATA, SIZE_NORMAL) \
X(TRIGGER, TRIGGER_DATA, SIZE_NORMAL) \
X(PLAYHEAD, PLAYHEAD_DATA, SIZE_NORMAL) \
X(PIVOT, PIVOT_DATA, SIZE_NORMAL) \
X(POINT, UNINTERPOLATED_DATA, SIZE_NORMAL) \
X(TARGET, TARGET_DATA, SIZE_HUGE)
enum Type
{
#define X(name, data, size) name,
LIST
#undef X
COUNT
};
struct Info
{
const char* data{};
size_t length{};
glm::ivec2 size{};
};
const Info ICONS[COUNT] = {
#define X(name, data, size) {data, std::strlen(data) - 1, size},
LIST
#undef X
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

81
src/layers.cpp Normal file
View File

@@ -0,0 +1,81 @@
#include "layers.h"
#include <ranges>
#include "imgui.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::layers
{
void Layers::update(DocumentManager& manager, Settings& settings, Resources& resources)
{
if (ImGui::Begin("Layers", &settings.windowIsLayers))
{
auto document = manager.get();
anm2::Anm2& anm2 = document->anm2;
auto& selection = document->selectedLayers;
storage.UserData = &selection;
storage.AdapterSetItemSelected = imgui::external_storage_set;
auto childSize = imgui::size_with_footer_get();
if (ImGui::BeginChild("##Layers Child", childSize, true))
{
ImGuiMultiSelectIO* io =
ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), anm2.content.layers.size());
storage.ApplyRequests(io);
for (auto& [id, layer] : anm2.content.layers)
{
auto isSelected = selection.contains(id);
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
imgui::selectable_input_text(std::format("#{} {}", id, layer.name),
std::format("###Document #{} Layer #{}", manager.selected, id), layer.name,
isSelected);
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(layer.name.c_str());
ImGui::TextUnformatted(std::format("ID: {}", id).c_str());
ImGui::TextUnformatted(std::format("Spritesheet ID: {}", layer.spritesheetID).c_str());
ImGui::PopFont();
ImGui::EndTooltip();
}
ImGui::PopID();
}
io = ImGui::EndMultiSelect();
storage.ApplyRequests(io);
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd, true);
ImGui::Button("Add", widgetSize);
imgui::set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd);
ImGui::SameLine();
std::set<int> unusedLayersIDs = anm2.layers_unused();
imgui::shortcut(settings.shortcutRemove, true);
ImGui::BeginDisabled(unusedLayersIDs.empty());
{
if (ImGui::Button("Remove Unused", widgetSize))
for (auto& id : unusedLayersIDs)
anm2.content.layers.erase(id);
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
}
}

17
src/layers.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "document_manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::layers
{
class Layers
{
ImGuiSelectionExternalStorage storage{};
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
};
}

93
src/loader.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "loader.h"
#include <imgui/backends/imgui_impl_opengl3.h>
#include <imgui/backends/imgui_impl_sdl3.h>
#include "filesystem.h"
#include "log.h"
using namespace anm2ed::log;
using namespace anm2ed::settings;
using namespace anm2ed::types;
namespace anm2ed::loader
{
std::string settings_path()
{
return filesystem::path_preferences_get() + "settings.ini";
}
Loader::Loader(int argc, const char** argv)
{
for (int i = 1; i < argc; i++)
arguments.emplace_back(argv[i]);
if (!SDL_Init(SDL_INIT_VIDEO))
{
logger.fatal(std::format("Could not initialize SDL! {}", SDL_GetError()));
isError = true;
return;
}
settings = Settings(settings_path());
window = SDL_CreateWindow("Anm2Ed", settings.windowSize.x, settings.windowSize.y,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL | SDL_WINDOW_HIGH_PIXEL_DENSITY);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
glContext = SDL_GL_CreateContext(window);
if (!glContext)
{
logger.fatal(std::format("Could not initialize OpenGL context! {}", SDL_GetError()));
isError = true;
return;
}
if (!gladLoadGLLoader((GLADloadproc)(SDL_GL_GetProcAddress)))
{
logger.fatal(std::format("Could not initialize OpenGL!"));
isError = true;
return;
}
logger.info(std::format("OpenGL {}", (const char*)glGetString(GL_VERSION)));
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glLineWidth(2.0f);
glDisable(GL_MULTISAMPLE);
glDisable(GL_DEPTH_TEST);
glDisable(GL_LINE_SMOOTH);
glClearColor(color::BLACK.r, color::BLACK.g, color::BLACK.b, color::BLACK.a);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui_ImplSDL3_InitForOpenGL(window, glContext);
ImGui_ImplOpenGL3_Init("#version 330");
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable | ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigWindowsMoveFromTitleBarOnly = true;
ImGui::LoadIniSettingsFromDisk(settings_path().c_str());
}
Loader::~Loader()
{
settings.save(settings_path(), ImGui::SaveIniSettingsToMemory(nullptr));
ImGui_ImplSDL3_Shutdown();
ImGui_ImplOpenGL3_Shutdown();
ImGui::DestroyContext();
SDL_GL_DestroyContext(glContext);
SDL_DestroyWindow(window);
SDL_Quit();
}
}

24
src/loader.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <vector>
#include <SDL3/SDL.h>
#include "settings.h"
namespace anm2ed::loader
{
class Loader
{
public:
SDL_Window* window{};
SDL_GLContext glContext{};
settings::Settings settings;
std::vector<std::string> arguments;
bool isError{};
Loader(int argc, const char** argv);
~Loader();
};
}

View File

@@ -1,56 +1,58 @@
#include "log.h"
inline std::ofstream logFile;
#include <print>
std::string log_path_get(void)
#include "filesystem.h"
#include "util.h"
using namespace anm2ed::filesystem;
using namespace anm2ed::util;
namespace anm2ed::log
{
return preferences_path_get() + LOG_PATH;
void Logger::write(const Level level, const std::string& message)
{
std::string formatted = std::format("{} {} {}", time::get("(%d-%B-%Y %I:%M:%S)"), LEVEL_STRINGS[level], message);
std::println("{}", formatted);
if (file.is_open()) file << formatted << '\n' << std::flush;
}
void Logger::info(const std::string& message)
{
write(INFO, message);
}
void Logger::warning(const std::string& message)
{
write(WARNING, message);
}
void Logger::error(const std::string& message)
{
write(ERROR, message);
}
void Logger::fatal(const std::string& message)
{
write(FATAL, message);
}
void Logger::open(const std::filesystem::path& path)
{
file.open(path, std::ios::out | std::ios::app);
}
Logger::Logger()
{
open(path_preferences_get() + "log.txt");
info("Initializing Anm2Ed");
}
Logger::~Logger()
{
info("Exiting Anm2Ed");
if (file.is_open()) file.close();
}
Logger logger;
}
void log_write(const std::string& string)
{
std::println("{}", string);
if (logFile.is_open())
{
logFile << string << '\n';
logFile.flush();
}
}
void log_init(void)
{
std::string logFilepath = log_path_get();
logFile.open(logFilepath, std::ios::out | std::ios::trunc);
if (!logFile) std::println("{}", std::format(LOG_INIT_ERROR, logFilepath));
}
void log_error(const std::string& error)
{
log_write(LOG_ERROR_FORMAT + error);
}
void log_info(const std::string& info)
{
log_write(LOG_INFO_FORMAT + info);
}
void log_warning(const std::string& warning)
{
log_write(LOG_WARNING_FORMAT + warning);
}
void log_imgui(const std::string& imgui)
{
log_write(LOG_IMGUI_FORMAT + imgui);
}
void log_command(const std::string& command)
{
log_write(LOG_COMMAND_FORMAT + command);
}
void log_free(void)
{
logFile.close();
}

View File

@@ -1,21 +1,44 @@
#pragma once
#include "COMMON.h"
#include <filesystem>
#include <fstream>
#define LOG_WARNING_FORMAT "[WARNING] "
#define LOG_ERROR_FORMAT "[ERROR] "
#define LOG_INFO_FORMAT "[INFO] "
#define LOG_IMGUI_FORMAT "[IMGUI] "
#define LOG_INIT_ERROR "[ERROR] Failed to open log file: {}"
#define LOG_COMMAND_FORMAT "[COMMAND] "
#define LOG_PATH "log.txt"
namespace anm2ed::log
{
#define LEVELS \
X(INFO, "[INFO]") \
X(WARNING, "[WARNING]") \
X(ERROR, "[ERROR]") \
X(FATAL, "[FATAL]")
std::string log_path_get(void);
void log_init(void);
void log_write(const std::string& file);
void log_error(const std::string& error);
void log_info(const std::string& info);
void log_warning(const std::string& warning);
void log_imgui(const std::string& imgui);
void log_command(const std::string& command);
void log_free(void);
enum Level
{
#define X(symbol, string) symbol,
LEVELS
#undef X
};
constexpr std::string_view LEVEL_STRINGS[] = {
#define X(symbol, string) string,
LEVELS
#undef X
};
#undef LEVELS
class Logger
{
std::ofstream file{};
public:
void write(const Level level, const std::string& message);
void info(const std::string& message);
void warning(const std::string& message);
void error(const std::string& message);
void fatal(const std::string& message);
void open(const std::filesystem::path& path);
Logger();
~Logger();
};
extern Logger logger;
}

View File

@@ -1,51 +1,19 @@
#include "main.h"
#include "loader.h"
#include "state.h"
static bool _anm2_rescale(const std::string& file, float scale) {
Anm2 anm2;
using namespace anm2ed::loader;
using namespace anm2ed::state;
if (!anm2_deserialize(&anm2, file, false))
return false;
anm2_scale(&anm2, scale);
return anm2_serialize(&anm2, file);
}
int main(int argc, const char** argv)
{
Loader loader(argc, argv);
int main(int argc, char* argv[]) {
State state;
if (loader.isError) return EXIT_FAILURE;
log_init();
State state(loader.window, loader.arguments);
if (argc > 0 && argv[1]) {
if (std::string(argv[1]) == ARGUMENT_RESCALE) {
if (argv[2] && argv[3]) {
if (_anm2_rescale(std::string(argv[2]), atof(argv[3]))) {
log_info(std::format(ARGUMENT_RESCALE_ANM2_INFO, argv[2], argv[3]));
return EXIT_SUCCESS;
} else
log_error(ARGUMENT_RESCALE_ANM2_ERROR);
} else
log_error(ARGUMENT_RESCALE_ARGUMENT_ERROR);
return EXIT_FAILURE;
} else if (std::string(argv[1]) == ARGUMENT_TEST && argv[2]) {
if (anm2_deserialize(&state.anm2, std::string(argv[2]), false))
return EXIT_SUCCESS;
return EXIT_FAILURE;
} else if (std::string(argv[1]) == ARGUMENT_TEST_GL && argv[2]) {
if (!sdl_init(&state, true))
return EXIT_FAILURE;
if (anm2_deserialize(&state.anm2, std::string(argv[2])))
return EXIT_SUCCESS;
return EXIT_FAILURE;
} else if (argv[1])
state.argument = argv[1];
}
init(&state);
while (state.isRunning)
loop(&state);
quit(&state);
while (!state.isQuit)
state.loop(loader.window, loader.settings);
return EXIT_SUCCESS;
}
}

View File

@@ -1,13 +0,0 @@
#pragma once
#include <SDL3/SDL_main.h>
#define ARGUMENT_TEST "--test"
#define ARGUMENT_TEST_GL "--test-gl"
#define ARGUMENT_RESCALE "--rescale"
#define ARGUMENT_RESCALE_ARGUMENT_ERROR "--rescale: specify both anm2 and scale arguments"
#define ARGUMENT_RESCALE_ANM2_ERROR "Unable to rescale anm2 {} by value {}. Make sure the file is valid."
#define ARGUMENT_RESCALE_ANM2_INFO "Scaled anm2 {} by {}"
#include "state.h"

81
src/math.cpp Normal file
View File

@@ -0,0 +1,81 @@
#include "math.h"
#include <glm/ext/matrix_transform.hpp>
#include <string>
using namespace glm;
namespace anm2ed::math
{
constexpr auto FLOAT_FORMAT_MAX_DECIMALS = 7;
constexpr auto FLOAT_FORMAT_EPSILON = 1e-7f;
constexpr float FLOAT_FORMAT_POW10[] = {1.f, 10.f, 100.f, 1000.f, 10000.f, 100000.f, 1000000.f, 10000000.f};
float round_nearest_multiple(float value, float multiple)
{
return (roundf((value) / (multiple)) * (multiple));
}
int float_decimals_needed(float value)
{
for (int decimalCount = 0; decimalCount <= FLOAT_FORMAT_MAX_DECIMALS; ++decimalCount)
{
auto scale = FLOAT_FORMAT_POW10[decimalCount];
auto rounded = roundf(value * scale) / scale;
if (fabsf(value - rounded) < FLOAT_FORMAT_EPSILON) return decimalCount;
}
return FLOAT_FORMAT_MAX_DECIMALS;
}
const char* float_format_get(float value)
{
static std::string formatString{};
int decimalCount = float_decimals_needed(value);
formatString = (decimalCount == 0) ? "%.0f" : ("%." + std::to_string(decimalCount) + "f");
return formatString.c_str();
}
const char* vec2_format_get(vec2& value)
{
static std::string formatString{};
int decimalCountX = float_decimals_needed(value.x);
int decimalCountY = float_decimals_needed(value.y);
int decimalCount = (decimalCountX > decimalCountY) ? decimalCountX : decimalCountY;
formatString = (decimalCount == 0) ? "%.0f" : ("%." + std::to_string(decimalCount) + "f");
return formatString.c_str();
}
mat4 quad_model_get(vec2 size, vec2 position, vec2 pivot, vec2 scale, float rotation)
{
vec2 scaleAbsolute = glm::abs(scale);
vec2 scaleSign = glm::sign(scale);
vec2 pivotScaled = pivot * scaleAbsolute;
vec2 sizeScaled = size * scaleAbsolute;
mat4 model(1.0f);
model = glm::translate(model, vec3(position - pivotScaled, 0.0f));
model = glm::translate(model, vec3(pivotScaled, 0.0f));
model = glm::scale(model, vec3(scaleSign, 1.0f));
model = glm::rotate(model, glm::radians(rotation), vec3(0, 0, 1));
model = glm::translate(model, vec3(-pivotScaled, 0.0f));
model = glm::scale(model, vec3(sizeScaled, 1.0f));
return model;
}
mat4 quad_model_parent_get(vec2 position, vec2 pivot, vec2 scale, float rotation)
{
vec2 scaleSign = glm::sign(scale);
vec2 scaleAbsolute = glm::abs(scale);
float handedness = (scaleSign.x * scaleSign.y) < 0.0f ? -1.0f : 1.0f;
mat4 local(1.0f);
local = glm::translate(local, vec3(pivot, 0.0f));
local = glm::scale(local, vec3(scaleSign, 1.0f));
local = glm::rotate(local, glm::radians(rotation) * handedness, vec3(0, 0, 1));
local = glm::translate(local, vec3(-pivot, 0.0f));
local = glm::scale(local, vec3(scaleAbsolute, 1.0f));
return glm::translate(mat4(1.0f), vec3(position, 0.0f)) * local;
}
}

46
src/math.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <array>
#include <glm/glm.hpp>
namespace anm2ed::math
{
template <typename T> constexpr T percent_to_unit(T value)
{
return value / 100.0f;
}
template <typename T> constexpr T unit_to_percent(T value)
{
return value * 100.0f;
}
constexpr float uint8_to_float(int value)
{
return (float)(value / 255.0f);
}
constexpr int float_to_uint8(float value)
{
return (int)(value * 255);
}
constexpr std::array<float, 16> uv_vertices_get(glm::vec2 uvMin, glm::vec2 uvMax)
{
return {0.0f, 0.0f, uvMin.x, uvMin.y, 1.0f, 0.0f, uvMax.x, uvMin.y,
1.0f, 1.0f, uvMax.x, uvMax.y, 0.0f, 1.0f, uvMin.x, uvMax.y};
}
float round_nearest_multiple(float value, float multiple);
int float_decimals_needed(float value);
const char* float_format_get(float value);
const char* vec2_format_get(glm::vec2& value);
glm::mat4 quad_model_get(glm::vec2 size = {}, glm::vec2 position = {}, glm::vec2 pivot = {},
glm::vec2 scale = glm::vec2(1.0f), float rotation = {});
glm::mat4 quad_model_parent_get(glm::vec2 position = {}, glm::vec2 pivot = {}, glm::vec2 scale = glm::vec2(1.0f),
float rotation = {});
}

80
src/nulls.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include "nulls.h"
#include <ranges>
#include "imgui.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::nulls
{
void Nulls::update(DocumentManager& manager, Settings& settings, Resources& resources)
{
if (ImGui::Begin("Nulls", &settings.windowIsNulls))
{
auto document = manager.get();
anm2::Anm2& anm2 = document->anm2;
auto& selection = document->selectedNulls;
storage.UserData = &selection;
storage.AdapterSetItemSelected = imgui::external_storage_set;
auto childSize = imgui::size_with_footer_get();
if (ImGui::BeginChild("##Nulls Child", childSize, true))
{
ImGuiMultiSelectIO* io =
ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), anm2.content.nulls.size());
storage.ApplyRequests(io);
for (auto& [id, null] : anm2.content.nulls)
{
const bool isSelected = selection.contains(id);
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
imgui::selectable_input_text(std::format("#{} {}", id, null.name),
std::format("###Document #{} Null #{}", manager.selected, id), null.name,
isSelected);
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(null.name.c_str());
ImGui::TextUnformatted(std::format("ID: {}", id).c_str());
ImGui::PopFont();
ImGui::EndTooltip();
}
ImGui::PopID();
}
io = ImGui::EndMultiSelect();
storage.ApplyRequests(io);
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd, true);
ImGui::Button("Add", widgetSize);
imgui::set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd);
ImGui::SameLine();
std::set<int> unusedNullsIDs = anm2.nulls_unused();
imgui::shortcut(settings.shortcutRemove, true);
ImGui::BeginDisabled(unusedNullsIDs.empty());
{
if (ImGui::Button("Remove Unused", widgetSize))
for (auto& id : unusedNullsIDs)
anm2.content.nulls.erase(id);
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
}
}

17
src/nulls.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "document_manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::nulls
{
class Nulls
{
ImGuiSelectionExternalStorage storage{};
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
};
}

43
src/onionskin.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include "onionskin.h"
#include <glm/gtc/type_ptr.hpp>
#include "imgui.h"
using namespace anm2ed::settings;
using namespace glm;
namespace anm2ed::onionskin
{
void Onionskin::update(Settings& settings)
{
if (ImGui::Begin("Onionskin", &settings.windowIsOnionskin))
{
auto order_configure = [&](const std::string& separator, int& frames, vec3& color)
{
ImGui::PushID(separator.c_str());
ImGui::SeparatorText(separator.c_str());
ImGui::InputInt("Frames", &frames, 1, 5);
frames = glm::clamp(frames, 0, 100);
ImGui::ColorEdit3("Color", value_ptr(color));
ImGui::PopID();
};
imgui::shortcut(settings.shortcutOnionskin, true, true);
ImGui::Checkbox("Enabled", &settings.onionskinIsEnabled);
order_configure("Before", settings.onionskinBeforeCount, settings.onionskinBeforeColor);
order_configure("After", settings.onionskinAfterCount, settings.onionskinAfterColor);
ImGui::Text("Order");
ImGui::SameLine();
ImGui::RadioButton("Before", &settings.onionskinDrawOrder, BELOW);
ImGui::SameLine();
ImGui::RadioButton("After", &settings.onionskinDrawOrder, ABOVE);
}
if (imgui::shortcut(settings.shortcutOnionskin)) settings.onionskinIsEnabled = !settings.onionskinIsEnabled;
ImGui::End();
}
}

18
src/onionskin.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include "settings.h"
namespace anm2ed::onionskin
{
enum Type
{
BELOW,
ABOVE
};
class Onionskin
{
public:
void update(settings::Settings& settings);
};
}

48
src/playback.cpp Normal file
View File

@@ -0,0 +1,48 @@
#include "playback.h"
#include <glm/common.hpp>
namespace anm2ed::playback
{
void Playback::toggle()
{
if (isFinished) time = 0.0f;
isFinished = false;
isPlaying = !isPlaying;
}
void Playback::clamp(int length)
{
time = glm::clamp(time, 0.0f, (float)length - 1.0f);
}
void Playback::tick(int fps, int length, bool isLoop)
{
if (isFinished) return;
time += (float)fps / 30.0f;
if (time >= (float)length)
{
if (isLoop)
time = 0.0f;
else
{
isPlaying = false;
isFinished = true;
}
}
}
void Playback::decrement(int length)
{
--time;
clamp(length);
}
void Playback::increment(int length)
{
++time;
clamp(length);
}
}

18
src/playback.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
namespace anm2ed::playback
{
class Playback
{
public:
float time{};
bool isPlaying{};
bool isFinished{};
void toggle();
void clamp(int length);
void tick(int fps, int length, bool isLoop);
void decrement(int length);
void increment(int length);
};
}

View File

@@ -1,259 +0,0 @@
#include "preview.h"
static void _preview_render_textures_free(Preview* self) {
for (auto& texture : self->renderFrames)
texture_free(&texture);
self->renderFrames.clear();
}
void preview_init(Preview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings) {
self->anm2 = anm2;
self->reference = reference;
self->resources = resources;
self->settings = settings;
canvas_init(&self->canvas, vec2());
}
void preview_tick(Preview* self) {
float& time = self->time;
Anm2Animation* animation = anm2_animation_from_reference(self->anm2, *self->reference);
if (animation) {
if (self->isPlaying) {
if (self->isRender) {
ivec2& size = self->canvas.size;
u32 framebufferPixelCount = size.x * size.y * TEXTURE_CHANNELS;
std::vector<u8> framebufferPixels(framebufferPixelCount);
Texture frameTexture;
glBindFramebuffer(GL_READ_FRAMEBUFFER, self->canvas.fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, framebufferPixels.data());
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
texture_from_rgba_init(&frameTexture, size, framebufferPixels.data());
self->renderFrames.push_back(frameTexture);
}
time += (float)self->anm2->fps / TICK_DELAY;
if (time >= (float)animation->frameNum - 1) {
if (self->isRender) {
self->isRender = false;
self->isRenderFinished = true;
time = 0.0f;
self->isPlaying = false;
} else {
if (self->settings->playbackIsLoop)
time = 0.0f;
else {
time = std::clamp(time, 0.0f, std::max(0.0f, (float)animation->frameNum - 1));
self->isPlaying = false;
}
}
}
}
if (self->settings->playbackIsClampPlayhead)
time = std::clamp(time, 0.0f, std::max(0.0f, (float)animation->frameNum - 1));
else
time = std::max(time, 0.0f);
}
}
void preview_draw(Preview* self) {
ivec2& gridSize = self->settings->previewGridSize;
ivec2& gridOffset = self->settings->previewGridOffset;
vec4& gridColor = self->settings->previewGridColor;
GLuint& shaderLine = self->resources->shaders[SHADER_LINE];
GLuint& shaderAxis = self->resources->shaders[SHADER_AXIS];
GLuint& shaderTexture = self->resources->shaders[SHADER_TEXTURE];
GLuint& shaderGrid = self->resources->shaders[SHADER_GRID];
mat4 transform = canvas_transform_get(&self->canvas, self->settings->previewPan, self->settings->previewZoom, ORIGIN_CENTER);
canvas_framebuffer_resize_check(&self->canvas);
canvas_bind(&self->canvas);
canvas_viewport_set(&self->canvas);
canvas_clear(self->settings->previewBackgroundColor);
if (self->settings->previewIsGrid)
canvas_grid_draw(&self->canvas, shaderGrid, transform, gridSize, gridOffset, gridColor);
if (self->settings->previewIsAxes)
canvas_axes_draw(&self->canvas, shaderAxis, transform, self->settings->previewAxesColor);
auto animation_draw = [&](int animationIndex) {
Anm2Animation* animation = anm2_animation_from_reference(self->anm2, {animationIndex});
if (!animation)
return;
auto root_draw = [&](Anm2Frame root, vec3 colorOffset = {}, float alphaOffset = {}, bool isOnionskin = {}) {
mat4 model = quad_model_get(PREVIEW_TARGET_SIZE, root.position, PREVIEW_TARGET_SIZE * 0.5f, PERCENT_TO_UNIT(root.scale), root.rotation);
mat4 rootTransform = transform * model;
vec4 color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : PREVIEW_ROOT_COLOR;
AtlasType atlas = self->settings->previewIsAltIcons ? ATLAS_TARGET_ALT : ATLAS_TARGET;
float vertices[] = ATLAS_UV_VERTICES(atlas);
canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, rootTransform, vertices, color);
};
auto layer_draw = [&](mat4 rootModel, int id, float time, vec3 colorOffset = {}, float alphaOffset = {}, bool isOnionskin = {}) {
Anm2Item& layerAnimation = animation->layerAnimations[id];
if (!layerAnimation.isVisible || layerAnimation.frames.size() <= 0)
return;
Anm2Frame frame;
anm2_frame_from_time(self->anm2, &frame, Anm2Reference{animationIndex, ANM2_LAYER, id}, time);
if (!frame.isVisible)
return;
mat4 model = quad_model_get(frame.size, frame.position, frame.pivot, PERCENT_TO_UNIT(frame.scale), frame.rotation);
mat4 layerTransform = transform * (rootModel * model);
vec3 frameColorOffset = frame.offsetRGB + colorOffset;
vec4 frameTint = frame.tintRGBA;
frameTint.a = std::max(0.0f, frameTint.a - alphaOffset);
Anm2Spritesheet* spritesheet = map_find(self->anm2->spritesheets, self->anm2->layers[id].spritesheetID);
if (!spritesheet)
return;
Texture& texture = spritesheet->texture;
if (texture.isInvalid)
return;
vec2 inset = 0.5f / vec2(texture.size);
vec2 uvMin = frame.crop / vec2(texture.size) + inset;
vec2 uvMax = (frame.crop + frame.size) / vec2(texture.size) - inset;
float vertices[] = UV_VERTICES(uvMin, uvMax);
canvas_texture_draw(&self->canvas, shaderTexture, texture.id, layerTransform, vertices, frameTint, frameColorOffset);
if (self->settings->previewIsBorder) {
vec4 borderColor = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : PREVIEW_BORDER_COLOR;
canvas_rect_draw(&self->canvas, shaderLine, layerTransform, borderColor);
}
if (self->settings->previewIsPivots) {
vec4 pivotColor = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : PREVIEW_PIVOT_COLOR;
float vertices[] = ATLAS_UV_VERTICES(ATLAS_PIVOT);
mat4 pivotModel = quad_model_get(CANVAS_PIVOT_SIZE, frame.position, CANVAS_PIVOT_SIZE * 0.5f, PERCENT_TO_UNIT(frame.scale), frame.rotation);
mat4 pivotTransform = transform * (rootModel * pivotModel);
canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, pivotTransform, vertices, pivotColor);
}
};
auto null_draw = [&](mat4 rootModel, int id, float time, vec3 colorOffset = {}, float alphaOffset = {}, bool isOnionskin = {}) {
Anm2Item& nullAnimation = animation->nullAnimations[id];
if (!nullAnimation.isVisible || nullAnimation.frames.size() <= 0)
return;
Anm2Frame frame;
anm2_frame_from_time(self->anm2, &frame, Anm2Reference{animationIndex, ANM2_NULL, id}, time);
if (!frame.isVisible)
return;
Anm2Null null = self->anm2->nulls[id];
vec4 color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset)
: (self->reference->itemType == ANM2_NULL && self->reference->itemID == id) ? PREVIEW_NULL_SELECTED_COLOR
: PREVIEW_NULL_COLOR;
vec2 size = null.isShowRect ? CANVAS_PIVOT_SIZE : PREVIEW_TARGET_SIZE;
AtlasType atlas = null.isShowRect ? ATLAS_SQUARE : self->settings->previewIsAltIcons ? ATLAS_TARGET_ALT : ATLAS_TARGET;
mat4 model = quad_model_get(size, frame.position, size * 0.5f, PERCENT_TO_UNIT(frame.scale), frame.rotation);
mat4 nullTransform = transform * (rootModel * model);
float vertices[] = ATLAS_UV_VERTICES(atlas);
canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, nullTransform, vertices, color);
if (null.isShowRect) {
mat4 rectModel = quad_model_get(PREVIEW_NULL_RECT_SIZE, frame.position, PREVIEW_NULL_RECT_SIZE * 0.5f, PERCENT_TO_UNIT(frame.scale), frame.rotation);
mat4 rectTransform = transform * (rootModel * rectModel);
canvas_rect_draw(&self->canvas, shaderLine, rectTransform, color);
}
};
auto base_draw = [&](float time, vec3 colorOffset = {}, float alphaOffset = {}, bool isOnionskin = {}) {
Anm2Frame root;
anm2_frame_from_time(self->anm2, &root, Anm2Reference{animationIndex, ANM2_ROOT}, time);
mat4 rootModel =
self->settings->previewIsRootTransform ? quad_model_parent_get(root.position, {}, PERCENT_TO_UNIT(root.scale), root.rotation) : mat4(1.0f);
if (self->settings->previewIsIcons && animation->rootAnimation.isVisible && root.isVisible)
root_draw(root, colorOffset, alphaOffset, isOnionskin);
for (auto id : animation->layerOrder)
layer_draw(rootModel, id, time, colorOffset, alphaOffset, isOnionskin);
if (self->settings->previewIsIcons)
for (auto& [id, _] : animation->nullAnimations)
null_draw(rootModel, id, time, colorOffset, alphaOffset, isOnionskin);
};
auto onionskin_draw = [&](int count, int direction, vec3 colorOffset) {
for (int i = 1; i <= count; i++) {
float time = self->time + (float)(direction * i);
float alphaOffset = (1.0f / (count + 1)) * i;
base_draw(time, colorOffset, alphaOffset, true);
}
};
auto onionskins_draw = [&]() {
if (!self->settings->onionskinIsEnabled)
return;
onionskin_draw(self->settings->onionskinBeforeCount, -1, self->settings->onionskinBeforeColorOffset);
onionskin_draw(self->settings->onionskinAfterCount, 1, self->settings->onionskinAfterColorOffset);
};
if (self->settings->onionskinDrawOrder == ONIONSKIN_BELOW)
onionskins_draw();
base_draw(self->time);
if (self->settings->onionskinDrawOrder == ONIONSKIN_ABOVE)
onionskins_draw();
};
animation_draw(self->reference->animationIndex);
animation_draw(self->animationOverlayID);
canvas_unbind();
}
void preview_render_start(Preview* self) {
self->isRender = true;
self->isPlaying = true;
self->time = 0.0f;
_preview_render_textures_free(self);
self->normalCanvasSize = self->canvas.size;
self->normalCanvasPan = self->settings->previewPan;
self->normalCanvasZoom = self->settings->previewZoom;
if (self->settings->renderIsUseAnimationBounds) {
vec4 rect = anm2_animation_rect_get(self->anm2, *self->reference, self->settings->previewIsRootTransform);
self->canvas.size = ivec2(ceilf(rect.z * self->settings->renderScale), ceilf(rect.w * self->settings->renderScale));
vec2 rectCenter = vec2(rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f);
self->settings->previewPan = -rectCenter * self->settings->renderScale;
self->settings->previewZoom = UNIT_TO_PERCENT(self->settings->renderScale);
}
}
void preview_render_end(Preview* self) {
self->isRender = false;
self->isPlaying = false;
self->isRenderFinished = false;
_preview_render_textures_free(self);
self->canvas.size = self->normalCanvasSize;
self->settings->previewPan = self->normalCanvasPan;
self->settings->previewZoom = self->normalCanvasZoom;
}
void preview_free(Preview* self) { canvas_free(&self->canvas); }

View File

@@ -1,52 +0,0 @@
#pragma once
#include "anm2.h"
#include "canvas.h"
#include "resources.h"
#include "settings.h"
const vec2 PREVIEW_SIZE = {2000, 2000};
const vec2 PREVIEW_CANVAS_SIZE = {2000, 2000};
const vec2 PREVIEW_CENTER = {0, 0};
#define PREVIEW_ZOOM_MIN 1
#define PREVIEW_ZOOM_MAX 1000
#define PREVIEW_ZOOM_STEP 25
#define PREVIEW_GRID_MIN 1
#define PREVIEW_GRID_MAX 1000
#define PREVIEW_GRID_OFFSET_MIN 0
#define PREVIEW_GRID_OFFSET_MAX 100
const vec2 PREVIEW_NULL_RECT_SIZE = {100, 100};
const vec2 PREVIEW_POINT_SIZE = {2, 2};
const vec2 PREVIEW_TARGET_SIZE = {16, 16};
const vec4 PREVIEW_BORDER_COLOR = COLOR_RED;
const vec4 PREVIEW_ROOT_COLOR = COLOR_GREEN;
const vec4 PREVIEW_NULL_COLOR = COLOR_BLUE;
const vec4 PREVIEW_NULL_SELECTED_COLOR = COLOR_RED;
const vec4 PREVIEW_PIVOT_COLOR = COLOR_RED;
struct Preview {
Anm2* anm2 = nullptr;
Anm2Reference* reference = nullptr;
Resources* resources = nullptr;
Settings* settings = nullptr;
int animationOverlayID = ID_NONE;
Canvas canvas;
vec2 normalCanvasSize{};
vec2 normalCanvasPan{};
float normalCanvasZoom{};
bool isPlaying = false;
bool isRender = false;
bool isRenderFinished = false;
bool isRenderCancelled = false;
std::vector<Texture> renderFrames;
float time{};
};
void preview_init(Preview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings);
void preview_draw(Preview* self);
void preview_tick(Preview* self);
void preview_free(Preview* self);
void preview_render_start(Preview* self);
void preview_render_end(Preview* self);

View File

@@ -1,19 +0,0 @@
#pragma once
#include "COMMON.h"
enum RenderType { RENDER_PNG, RENDER_GIF, RENDER_WEBM, RENDER_MP4, RENDER_COUNT };
const inline std::string RENDER_TYPE_STRINGS[] = {
"PNG Images",
"GIF image",
"WebM video",
"MP4 video",
};
const inline std::string RENDER_EXTENSIONS[RENDER_COUNT] = {
".png",
".gif",
".webm",
".mp4",
};

View File

@@ -1,16 +1,21 @@
#include "resources.h"
#include "RESOURCE.h"
#include <ranges>
void resources_init(Resources* self) {
texture_from_memory_init(&self->atlas, TEXTURE_ATLAS_SIZE, TEXTURE_ATLAS, TEXTURE_ATLAS_LENGTH);
using namespace anm2ed::texture;
using namespace anm2ed::shader;
using namespace anm2ed::font;
for (int i = 0; i < SHADER_COUNT; i++)
shader_init(&self->shaders[i], SHADER_DATA[i].vertex, SHADER_DATA[i].fragment);
}
namespace anm2ed::resources
{
Resources::Resources()
{
for (auto [i, font] : std::views::enumerate(font::FONTS))
fonts[i] = Font((void*)font.data, font.length, font::SIZE);
void resources_free(Resources* self) {
for (auto& shader : self->shaders)
shader_free(&shader);
for (auto [i, icon] : std::views::enumerate(icon::ICONS))
icons[i] = Texture(icon.data, icon.length, icon.size);
texture_free(&self->atlas);
for (auto [i, shader] : std::views::enumerate(shader::SHADERS))
shaders[i] = Shader(shader.vertex, shader.fragment);
};
}

View File

@@ -1,15 +1,21 @@
#pragma once
#include "RESOURCE.h"
#include <imgui/imgui.h>
#include "font.h"
#include "icon.h"
#include "shader.h"
#include "texture.h"
#define RESOURCES_TEXTURES_FREE_INFO "Freed texture resources"
namespace anm2ed::resources
{
class Resources
{
public:
font::Font fonts[font::COUNT]{};
texture::Texture icons[icon::COUNT]{};
shader::Shader shaders[shader::COUNT]{};
struct Resources {
GLuint shaders[SHADER_COUNT];
Texture atlas;
};
void resources_init(Resources* self);
void resources_free(Resources* self);
Resources();
};
}

View File

@@ -1,305 +1,314 @@
#include "settings.h"
static void _settings_setting_load(Settings* self, const std::string& line) {
for (int i = 0; i < SETTINGS_COUNT; i++) {
const auto& entry = SETTINGS_ENTRIES[i];
const std::string& key = entry.key;
void* target = (u8*)self + entry.offset;
#include "filesystem.h"
#include "log.h"
auto match_key = [&](const std::string& full) -> const char* {
if (!line.starts_with(full))
return nullptr;
using namespace anm2ed::filesystem;
using namespace anm2ed::log;
using namespace glm;
size_t p = full.size();
while (p < line.size() && std::isspace((u8)line[p]))
++p;
if (p < line.size() && line[p] == '=')
return line.c_str() + p + 1;
return nullptr;
};
namespace anm2ed::settings
{
constexpr auto IMGUI_DEFAULT = R"(
# Dear ImGui
[Window][## Window]
Pos=0,32
Size=1600,868
Collapsed=0
const char* value = nullptr;
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
switch (entry.type) {
case TYPE_INT:
if ((value = match_key(key))) {
*(int*)target = std::atoi(value);
return;
}
break;
case TYPE_BOOL:
if ((value = match_key(key))) {
*(bool*)target = string_to_bool(value);
return;
}
break;
case TYPE_FLOAT:
if ((value = match_key(key))) {
*(f32*)target = std::atof(value);
return;
}
break;
case TYPE_STRING:
if ((value = match_key(key))) {
*(std::string*)target = value;
return;
}
break;
case TYPE_IVEC2: {
ivec2* v = (ivec2*)target;
if ((value = match_key(key + "X"))) {
v->x = std::atoi(value);
return;
}
if ((value = match_key(key + "Y"))) {
v->y = std::atoi(value);
return;
}
break;
[Window][Tools]
Pos=8,40
Size=38,516
Collapsed=0
DockId=0x0000000B,0
[Window][Animations]
Pos=1289,307
Size=303,249
Collapsed=0
DockId=0x0000000A,0
[Window][Events]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,2
[Window][Spritesheets]
Pos=1289,40
Size=303,265
Collapsed=0
DockId=0x00000009,0
[Window][Animation Preview]
Pos=48,40
Size=907,516
Collapsed=0
DockId=0x0000000C,0
[Window][Spritesheet Editor]
Pos=48,40
Size=907,516
Collapsed=0
DockId=0x0000000C,1
[Window][Timeline]
Pos=8,558
Size=1584,334
Collapsed=0
DockId=0x00000004,0
[Window][Frame Properties]
Pos=957,40
Size=330,222
Collapsed=0
DockId=0x00000007,0
[Window][Onionskin]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,3
[Window][Layers]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,0
[Window][Nulls]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,1
[Docking][Data]
DockSpace ID=0xFC02A410 Window=0x0E46F4F7 Pos=8,40 Size=1584,852 Split=Y
DockNode ID=0x00000003 Parent=0xFC02A410 SizeRef=1902,680 Split=X
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=1017,1016 Split=X Selected=0x024430EF
DockNode ID=0x00000005 Parent=0x00000001 SizeRef=1264,654 Split=X Selected=0x024430EF
DockNode ID=0x0000000B Parent=0x00000005 SizeRef=38,654 Selected=0x18A5FDB9
DockNode ID=0x0000000C Parent=0x00000005 SizeRef=1224,654 CentralNode=1 Selected=0x024430EF
DockNode ID=0x00000006 Parent=0x00000001 SizeRef=330,654 Split=Y Selected=0x754E368F
DockNode ID=0x00000007 Parent=0x00000006 SizeRef=631,293 Selected=0x754E368F
DockNode ID=0x00000008 Parent=0x00000006 SizeRef=631,385 Selected=0xCD8384B1
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=303,1016 Split=Y Selected=0x4EFD0020
DockNode ID=0x00000009 Parent=0x00000002 SizeRef=634,349 Selected=0x4EFD0020
DockNode ID=0x0000000A Parent=0x00000002 SizeRef=634,329 Selected=0xC1986EE2
DockNode ID=0x00000004 Parent=0xFC02A410 SizeRef=1902,334 Selected=0x4F89F0DC
)";
Settings::Settings() = default;
Settings::Settings(const std::string& path)
{
if (path_is_exist(path))
logger.info(std::format("Using settings from: {}", path));
else
{
logger.warning("Settings file does not exist; using default");
save(path, IMGUI_DEFAULT);
}
case TYPE_IVEC2_WH: {
ivec2* v = (ivec2*)target;
if ((value = match_key(key + "W"))) {
v->x = std::atoi(value);
return;
}
if ((value = match_key(key + "H"))) {
v->y = std::atoi(value);
return;
}
break;
};
case TYPE_VEC2: {
vec2* v = (vec2*)target;
if ((value = match_key(key + "X"))) {
v->x = std::atof(value);
return;
}
if ((value = match_key(key + "Y"))) {
v->y = std::atof(value);
return;
}
break;
}
case TYPE_VEC2_WH: {
vec2* v = (vec2*)target;
if ((value = match_key(key + "W"))) {
v->x = std::atof(value);
return;
}
if ((value = match_key(key + "H"))) {
v->y = std::atof(value);
return;
}
break;
};
case TYPE_VEC3: {
vec3* v = (vec3*)target;
if ((value = match_key(key + "R"))) {
v->x = std::atof(value);
return;
}
if ((value = match_key(key + "G"))) {
v->y = std::atof(value);
return;
}
if ((value = match_key(key + "B"))) {
v->z = std::atof(value);
return;
}
break;
}
case TYPE_VEC4: {
vec4* v = (vec4*)target;
if ((value = match_key(key + "R"))) {
v->x = std::atof(value);
return;
}
if ((value = match_key(key + "G"))) {
v->y = std::atof(value);
return;
}
if ((value = match_key(key + "B"))) {
v->z = std::atof(value);
return;
}
if ((value = match_key(key + "A"))) {
v->w = std::atof(value);
return;
}
break;
}
default:
break;
}
}
log_warning(std::format(SETTINGS_VALUE_INIT_WARNING, line));
}
std::string settings_path_get(void) {
std::string filePath = preferences_path_get() + SETTINGS_PATH;
return filePath;
}
static void _settings_setting_write(Settings* self, std::ostream& out, SettingsEntry entry) {
u8* selfPointer = (u8*)self;
std::string value;
switch (entry.type) {
case TYPE_INT:
value = std::format("{}", *(int*)(selfPointer + entry.offset));
out << entry.key << "=" << value << "\n";
break;
case TYPE_BOOL:
value = std::format("{}", *(bool*)(selfPointer + entry.offset));
out << entry.key << "=" << value << "\n";
break;
case TYPE_FLOAT:
value = std::format("{:.3f}", *(f32*)(selfPointer + entry.offset));
out << entry.key << "=" << value << "\n";
break;
case TYPE_STRING: {
const std::string data = *reinterpret_cast<const std::string*>(selfPointer + entry.offset);
if (!data.empty())
out << entry.key << "=" << data.c_str() << "\n";
break;
}
case TYPE_IVEC2: {
ivec2* data = (ivec2*)(selfPointer + entry.offset);
out << entry.key << "X=" << data->x << "\n";
out << entry.key << "Y=" << data->y << "\n";
break;
}
case TYPE_IVEC2_WH: {
ivec2* data = (ivec2*)(selfPointer + entry.offset);
out << entry.key << "W=" << data->x << "\n";
out << entry.key << "H=" << data->y << "\n";
break;
}
case TYPE_VEC2: {
vec2* data = (vec2*)(selfPointer + entry.offset);
out << entry.key << "X=" << std::format(SETTINGS_FLOAT_FORMAT, data->x) << "\n";
out << entry.key << "Y=" << std::format(SETTINGS_FLOAT_FORMAT, data->y) << "\n";
break;
}
case TYPE_VEC2_WH: {
vec2* data = (vec2*)(selfPointer + entry.offset);
out << entry.key << "W=" << std::format(SETTINGS_FLOAT_FORMAT, data->x) << "\n";
out << entry.key << "H=" << std::format(SETTINGS_FLOAT_FORMAT, data->y) << "\n";
break;
}
case TYPE_VEC3: {
vec3* data = (vec3*)(selfPointer + entry.offset);
out << entry.key << "R=" << std::format(SETTINGS_FLOAT_FORMAT, data->r) << "\n";
out << entry.key << "G=" << std::format(SETTINGS_FLOAT_FORMAT, data->g) << "\n";
out << entry.key << "B=" << std::format(SETTINGS_FLOAT_FORMAT, data->b) << "\n";
break;
}
case TYPE_VEC4: {
vec4* data = (vec4*)(selfPointer + entry.offset);
out << entry.key << "R=" << std::format(SETTINGS_FLOAT_FORMAT, data->r) << "\n";
out << entry.key << "G=" << std::format(SETTINGS_FLOAT_FORMAT, data->g) << "\n";
out << entry.key << "B=" << std::format(SETTINGS_FLOAT_FORMAT, data->b) << "\n";
out << entry.key << "A=" << std::format(SETTINGS_FLOAT_FORMAT, data->a) << "\n";
break;
}
default:
break;
}
}
void settings_save(Settings* self) {
const std::string path = settings_path_get();
const std::filesystem::path filesystemPath(path);
const std::filesystem::path directory = filesystemPath.parent_path();
if (!directory.empty()) {
std::error_code errorCode;
std::filesystem::create_directories(directory, errorCode);
if (errorCode) {
log_error(std::format(SETTINGS_DIRECTORY_ERROR, directory.string(), errorCode.message()));
std::ifstream file(path);
if (!file.is_open())
{
logger.error(std::format("Failed to open settings file: {}", path));
return;
}
}
std::string data;
if (std::filesystem::exists(filesystemPath)) {
if (std::ifstream in(path, std::ios::binary); in)
data.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
std::string line{};
std::filesystem::path temp = filesystemPath;
temp += SETTINGS_TEMPORARY_EXTENSION;
auto stream_assign = [](auto& dest, std::istringstream& ss) { ss >> dest; };
std::ofstream out(temp, std::ios::binary | std::ios::trunc);
if (!out) {
log_error(std::format(SETTINGS_INIT_ERROR, temp.string()));
return;
}
auto value_set = [&](auto& dest, std::istringstream& ss)
{
using T = std::decay_t<decltype(dest)>;
out << SETTINGS_SECTION << "\n";
for (int i = 0; i < SETTINGS_COUNT; i++)
_settings_setting_write(self, out, SETTINGS_ENTRIES[i]);
if constexpr (std::is_same_v<T, bool>)
{
std::string val;
stream_assign(val, ss);
dest = (val == "true" || val == "1");
}
else if constexpr (std::is_same_v<T, std::string>)
std::getline(ss, dest);
else
stream_assign(dest, ss);
};
out << "\n" << SETTINGS_SECTION_IMGUI << "\n";
out << data;
auto entry_load =
[&](const std::string& key, std::istringstream& ss, const std::string& name, auto& value, std::string_view type)
{
using T = std::decay_t<decltype(value)>;
out.flush();
auto is_match = [&](const char* suffix) { return key == name + suffix; };
if (!out.good()) {
log_error(std::format(SETTINGS_SAVE_ERROR, temp.string()));
return;
}
if constexpr (std::is_same_v<T, ivec2> || std::is_same_v<T, vec2>)
{
if (type.ends_with("_WH"))
{
if (is_match("W"))
{
stream_assign(value.x, ss);
return true;
}
if (is_match("H"))
{
stream_assign(value.y, ss);
return true;
}
}
else
{
if (is_match("X"))
{
stream_assign(value.x, ss);
return true;
}
if (is_match("Y"))
{
stream_assign(value.y, ss);
return true;
}
}
}
else if constexpr (std::is_same_v<T, vec3>)
{
if (is_match("R"))
{
stream_assign(value.x, ss);
return true;
}
if (is_match("G"))
{
stream_assign(value.y, ss);
return true;
}
if (is_match("B"))
{
stream_assign(value.z, ss);
return true;
}
}
else if constexpr (std::is_same_v<T, vec4>)
{
if (is_match("R"))
{
stream_assign(value.x, ss);
return true;
}
if (is_match("G"))
{
stream_assign(value.y, ss);
return true;
}
if (is_match("B"))
{
stream_assign(value.z, ss);
return true;
}
if (is_match("A"))
{
stream_assign(value.w, ss);
return true;
}
}
else
{
if (key == name)
{
value_set(value, ss);
return true;
}
}
out.close();
return false;
};
std::error_code errorCode;
std::filesystem::rename(temp, filesystemPath, errorCode);
if (errorCode) {
// Windows can block rename if target exists; try remove+rename
std::filesystem::remove(filesystemPath, errorCode);
errorCode = {};
std::filesystem::rename(temp, filesystemPath, errorCode);
if (errorCode) {
log_error(std::format(SETTINGS_SAVE_FINALIZE_ERROR, filesystemPath.string(), errorCode.message()));
std::filesystem::remove(temp);
return;
while (std::getline(file, line))
{
if (line == "[Settings]" || line.empty()) continue;
if (line == "# Dear ImGui") break;
auto eq = line.find('=');
if (eq == std::string::npos) continue;
auto key = line.substr(0, eq);
std::istringstream ss(line.substr(eq + 1));
#define X(symbol, name, string, type, ...) \
if (entry_load(key, ss, #name, name, #type)) continue;
SETTINGS_MEMBERS SETTINGS_SHORTCUTS SETTINGS_WINDOWS
#undef X
}
file.close();
}
log_info(std::format(SETTINGS_SAVE_INFO, path));
}
void Settings::save(const std::string& path, const std::string& imguiData)
{
std::ofstream file(path, std::ios::out | std::ios::binary);
file << "[Settings]\n";
void settings_init(Settings* self) {
const std::string path = settings_path_get();
std::ifstream file(path, std::ios::binary);
auto value_save = [&](const std::string& key, const auto& value)
{
using T = std::decay_t<decltype(value)>;
if (file)
log_info(std::format(SETTINGS_INIT_INFO, path));
else {
log_warning(std::format(SETTINGS_INIT_WARNING, path));
settings_save(self);
std::ofstream out(path, std::ios::binary | std::ios::app);
out << SETTINGS_IMGUI_DEFAULT;
out.flush();
out.close();
file.open(path, std::ios::binary);
}
if constexpr (std::is_same_v<T, bool>)
file << key << "=" << (value ? "true" : "false") << "\n";
else
file << key << "=" << value << "\n";
};
auto entry_save = [&](const std::string& name, const auto& value, const std::string_view type)
{
using T = std::decay_t<decltype(value)>;
std::string line;
bool inSettingsSection = false;
if constexpr (std::is_same_v<T, ivec2> || std::is_same_v<T, vec2>)
{
if (type.ends_with("_WH"))
{
value_save(name + "W", value.x);
value_save(name + "H", value.y);
}
else
{
value_save(name + "X", value.x);
value_save(name + "Y", value.y);
}
}
else if constexpr (std::is_same_v<T, vec3>)
{
value_save(name + "R", value.x);
value_save(name + "G", value.y);
value_save(name + "B", value.z);
}
else if constexpr (std::is_same_v<T, vec4>)
{
value_save(name + "R", value.x);
value_save(name + "G", value.y);
value_save(name + "B", value.z);
value_save(name + "A", value.w);
}
else
value_save(name, value);
};
while (std::getline(file, line)) {
if (line == SETTINGS_SECTION) {
inSettingsSection = true;
continue;
}
if (line.empty())
continue;
if (line == SETTINGS_SECTION_IMGUI)
break;
if (inSettingsSection)
_settings_setting_load(self, line);
#define X(symbol, name, string, type, ...) entry_save(#name, name, #type);
SETTINGS_MEMBERS SETTINGS_SHORTCUTS SETTINGS_WINDOWS
#undef X
file
<< "\n# Dear ImGui\n"
<< imguiData;
file.flush();
file.close();
}
}

View File

@@ -1,366 +1,252 @@
#pragma once
#include <string>
#include <glm/glm.hpp>
#include "anm2.h"
#include "render.h"
#include "tool.h"
#define SETTINGS_SECTION "[Settings]"
#define SETTINGS_SECTION_IMGUI "# Dear ImGui"
#define SETTINGS_INIT_WARNING "Unable to read settings file: {}; using default settings"
#define SETTINGS_INIT_ERROR "Unable to read settings file: {}"
#define SETTINGS_SAVE_ERROR "Failed to write settings file: {}"
#define SETTINGS_SAVE_FINALIZE_ERROR "Failed to write settings file: {} ({})"
#define SETTINGS_VALUE_INIT_WARNING "Unknown setting: {}"
#define SETTINGS_FLOAT_FORMAT "{:.3f}"
#define SETTINGS_INIT_INFO "Initialized settings from: {}"
#define SETTINGS_DIRECTORY_ERROR "Failed to create settings directory: {} ({})"
#define SETTINGS_SAVE_INFO "Saved settings to: {}"
#define SETTINGS_FOLDER "anm2ed"
#define SETTINGS_PATH "settings.ini"
#define SETTINGS_TEMPORARY_EXTENSION ".tmp"
#include "types.h"
namespace anm2ed::settings
{
#ifdef _WIN32
#define SETTINGS_RENDER_FFMPEG_PATH_VALUE_DEFAULT "C:\\ffmpeg\\bin\\ffmpeg.exe"
constexpr auto FFMPEG_PATH_DEFAULT = "C:\\ffmpeg\\bin\\ffmpeg.exe";
#else
#define SETTINGS_RENDER_FFMPEG_PATH_VALUE_DEFAULT "/usr/bin/ffmpeg"
constexpr auto FFMPEG_PATH_DEFAULT = "/usr/bin/ffmpeg";
#endif
#define SETTINGS_LIST \
/* Symbol / Name / Type / Default */ \
X(WINDOW_SIZE, windowSize, TYPE_IVEC2_WH, {1600, 900}) \
X(IS_VSYNC, isVsync, TYPE_BOOL, true) \
X(DISPLAY_SCALE, displayScale, TYPE_FLOAT, 1.0f) \
\
X(HOTKEY_CENTER_VIEW, hotkeyCenterView, TYPE_STRING, "Home") \
X(HOTKEY_FIT, hotkeyFit, TYPE_STRING, "F") \
X(HOTKEY_ZOOM_IN, hotkeyZoomIn, TYPE_STRING, "Ctrl++") \
X(HOTKEY_ZOOM_OUT, hotkeyZoomOut, TYPE_STRING, "Ctrl+-") \
X(HOTKEY_PLAY_PAUSE, hotkeyPlayPause, TYPE_STRING, "Space") \
X(HOTKEY_ONIONSKIN, hotkeyOnionskin, TYPE_STRING, "O") \
X(HOTKEY_NEW, hotkeyNew, TYPE_STRING, "Ctrl+N") \
X(HOTKEY_OPEN, hotkeyOpen, TYPE_STRING, "Ctrl+O") \
X(HOTKEY_SAVE, hotkeySave, TYPE_STRING, "Ctrl+S") \
X(HOTKEY_SAVE_AS, hotkeySaveAs, TYPE_STRING, "Ctrl+Shift+S") \
X(HOTKEY_EXIT, hotkeyExit, TYPE_STRING, "Alt+F4") \
X(HOTKEY_SHORTEN_FRAME, hotkeyShortenFrame, TYPE_STRING, "F4") \
X(HOTKEY_EXTEND_FRAME, hotkeyExtendFrame, TYPE_STRING, "F5") \
X(HOTKEY_INSERT_FRAME, hotkeyInsertFrame, TYPE_STRING, "F6") \
X(HOTKEY_PREVIOUS_FRAME, hotkeyPreviousFrame, TYPE_STRING, "Comma") \
X(HOTKEY_NEXT_FRAME, hotkeyNextFrame, TYPE_STRING, "Period") \
X(HOTKEY_PAN, hotkeyPan, TYPE_STRING, "P") \
X(HOTKEY_MOVE, hotkeyMove, TYPE_STRING, "V") \
X(HOTKEY_ROTATE, hotkeyRotate, TYPE_STRING, "R") \
X(HOTKEY_SCALE, hotkeyScale, TYPE_STRING, "S") \
X(HOTKEY_CROP, hotkeyCrop, TYPE_STRING, "C") \
X(HOTKEY_DRAW, hotkeyDraw, TYPE_STRING, "B") \
X(HOTKEY_ERASE, hotkeyErase, TYPE_STRING, "E") \
X(HOTKEY_COLOR_PICKER, hotkeyColorPicker, TYPE_STRING, "I") \
X(HOTKEY_UNDO, hotkeyUndo, TYPE_STRING, "Ctrl+Z") \
X(HOTKEY_REDO, hotkeyRedo, TYPE_STRING, "Ctrl+Shift+Z") \
X(HOTKEY_COPY, hotkeyCopy, TYPE_STRING, "Ctrl+C") \
X(HOTKEY_CUT, hotkeyCut, TYPE_STRING, "Ctrl+X") \
X(HOTKEY_PASTE, hotkeyPaste, TYPE_STRING, "Ctrl+V") \
X(HOTKEY_SELECT_ALL, hotkeySelectAll, TYPE_STRING, "Ctrl+A") \
X(HOTKEY_SELECT_NONE, hotkeySelectNone, TYPE_STRING, "Ctrl+Shift+A") \
\
X(PLAYBACK_IS_LOOP, playbackIsLoop, TYPE_BOOL, true) \
X(PLAYBACK_IS_CLAMP_PLAYHEAD, playbackIsClampPlayhead, TYPE_BOOL, true) \
\
X(CHANGE_IS_CROP, changeIsCrop, TYPE_BOOL, false) \
X(CHANGE_IS_SIZE, changeIsSize, TYPE_BOOL, false) \
X(CHANGE_IS_POSITION, changeIsPosition, TYPE_BOOL, false) \
X(CHANGE_IS_PIVOT, changeIsPivot, TYPE_BOOL, false) \
X(CHANGE_IS_SCALE, changeIsScale, TYPE_BOOL, false) \
X(CHANGE_IS_ROTATION, changeIsRotation, TYPE_BOOL, false) \
X(CHANGE_IS_DELAY, changeIsDelay, TYPE_BOOL, false) \
X(CHANGE_IS_TINT, changeIsTint, TYPE_BOOL, false) \
X(CHANGE_IS_COLOR_OFFSET, changeIsColorOffset, TYPE_BOOL, false) \
X(CHANGE_IS_VISIBLE_SET, changeIsVisibleSet, TYPE_BOOL, false) \
X(CHANGE_IS_INTERPOLATED_SET, changeIsInterpolatedSet, TYPE_BOOL, false) \
X(CHANGE_IS_FROM_SELECTED_FRAME, changeIsFromSelectedFrame, TYPE_BOOL, false) \
X(CHANGE_CROP, changeCrop, TYPE_VEC2, {}) \
X(CHANGE_SIZE, changeSize, TYPE_VEC2, {}) \
X(CHANGE_POSITION, changePosition, TYPE_VEC2, {}) \
X(CHANGE_PIVOT, changePivot, TYPE_VEC2, {}) \
X(CHANGE_SCALE, changeScale, TYPE_VEC2, {}) \
X(CHANGE_ROTATION, changeRotation, TYPE_FLOAT, 0.0f) \
X(CHANGE_DELAY, changeDelay, TYPE_INT, 0) \
X(CHANGE_TINT, changeTint, TYPE_VEC4, {}) \
X(CHANGE_COLOR_OFFSET, changeColorOffset, TYPE_VEC3, {}) \
X(CHANGE_IS_VISIBLE, changeIsVisible, TYPE_BOOL, false) \
X(CHANGE_IS_INTERPOLATED, changeIsInterpolated, TYPE_BOOL, false) \
X(CHANGE_NUMBER_FRAMES, changeNumberFrames, TYPE_INT, 1) \
\
X(SCALE_VALUE, scaleValue, TYPE_FLOAT, 1.0f) \
\
X(PREVIEW_IS_AXES, previewIsAxes, TYPE_BOOL, true) \
X(PREVIEW_IS_GRID, previewIsGrid, TYPE_BOOL, true) \
X(PREVIEW_IS_ROOT_TRANSFORM, previewIsRootTransform, TYPE_BOOL, true) \
X(PREVIEW_IS_TRIGGERS, previewIsTriggers, TYPE_BOOL, true) \
X(PREVIEW_IS_PIVOTS, previewIsPivots, TYPE_BOOL, false) \
X(PREVIEW_IS_ICONS, previewIsIcons, TYPE_BOOL, true) \
X(PREVIEW_IS_BORDER, previewIsBorder, TYPE_BOOL, false) \
X(PREVIEW_IS_ALT_ICONS, previewIsAltIcons, TYPE_BOOL, false) \
X(PREVIEW_OVERLAY_TRANSPARENCY, previewOverlayTransparency, TYPE_FLOAT, 255.0f) \
X(PREVIEW_ZOOM, previewZoom, TYPE_FLOAT, 200.0f) \
X(PREVIEW_PAN, previewPan, TYPE_VEC2, {}) \
X(PREVIEW_GRID_SIZE, previewGridSize, TYPE_IVEC2, {32, 32}) \
X(PREVIEW_GRID_OFFSET, previewGridOffset, TYPE_IVEC2, {}) \
X(PREVIEW_GRID_COLOR, previewGridColor, TYPE_VEC4, {1.0, 1.0, 1.0, 0.125}) \
X(PREVIEW_AXES_COLOR, previewAxesColor, TYPE_VEC4, {1.0, 1.0, 1.0, 0.125}) \
X(PREVIEW_BACKGROUND_COLOR, previewBackgroundColor, TYPE_VEC4, {0.113, 0.184, 0.286, 1.0}) \
\
X(PROPERTIES_IS_ROUND, propertiesIsRound, TYPE_BOOL, false) \
\
X(GENERATE_START_POSITION, generateStartPosition, TYPE_IVEC2, {}) \
X(GENERATE_SIZE, generateSize, TYPE_IVEC2, {64, 64}) \
X(GENERATE_PIVOT, generatePivot, TYPE_IVEC2, {32, 32}) \
X(GENERATE_ROWS, generateRows, TYPE_INT, 4) \
X(GENERATE_COLUMNS, generateColumns, TYPE_INT, 4) \
X(GENERATE_COUNT, generateCount, TYPE_INT, 16) \
X(GENERATE_DELAY, generateDelay, TYPE_INT, 1) \
\
X(EDITOR_IS_GRID, editorIsGrid, TYPE_BOOL, true) \
X(EDITOR_IS_GRID_SNAP, editorIsGridSnap, TYPE_BOOL, true) \
X(EDITOR_IS_BORDER, editorIsBorder, TYPE_BOOL, true) \
X(EDITOR_ZOOM, editorZoom, TYPE_FLOAT, 200.0f) \
X(EDITOR_PAN, editorPan, TYPE_VEC2, {0.0, 0.0}) \
X(EDITOR_GRID_SIZE, editorGridSize, TYPE_IVEC2, {32, 32}) \
X(EDITOR_GRID_OFFSET, editorGridOffset, TYPE_IVEC2, {32, 32}) \
X(EDITOR_GRID_COLOR, editorGridColor, TYPE_VEC4, {1.0, 1.0, 1.0, 0.125}) \
X(EDITOR_BACKGROUND_COLOR, editorBackgroundColor, TYPE_VEC4, {0.113, 0.184, 0.286, 1.0}) \
\
X(MERGE_TYPE, mergeType, TYPE_INT, ANM2_MERGE_APPEND) \
X(MERGE_IS_DELETE_ANIMATIONS_AFTER, mergeIsDeleteAnimationsAfter, TYPE_BOOL, false) \
\
X(BAKE_INTERVAL, bakeInterval, TYPE_INT, 1) \
X(BAKE_IS_ROUND_SCALE, bakeIsRoundScale, TYPE_BOOL, true) \
X(BAKE_IS_ROUND_ROTATION, bakeIsRoundRotation, TYPE_BOOL, true) \
\
X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, TYPE_INT, ANM2_LAYER) \
X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, TYPE_BOOL, true) \
\
X(ONIONSKIN_IS_ENABLED, onionskinIsEnabled, TYPE_BOOL, false) \
X(ONIONSKIN_DRAW_ORDER, onionskinDrawOrder, TYPE_INT, ONIONSKIN_BELOW) \
X(ONIONSKIN_BEFORE_COUNT, onionskinBeforeCount, TYPE_INT, 0) \
X(ONIONSKIN_AFTER_COUNT, onionskinAfterCount, TYPE_INT, 0) \
X(ONIONSKIN_BEFORE_COLOR_OFFSET, onionskinBeforeColorOffset, TYPE_VEC3, COLOR_RED) \
X(ONIONSKIN_AFTER_COLOR_OFFSET, onionskinAfterColorOffset, TYPE_VEC3, COLOR_BLUE) \
\
X(TOOL, tool, TYPE_INT, TOOL_PAN) \
X(TOOL_COLOR, toolColor, TYPE_VEC4, {1.0, 1.0, 1.0, 1.0}) \
\
X(RENDER_TYPE, renderType, TYPE_INT, RENDER_PNG) \
X(RENDER_PATH, renderPath, TYPE_STRING, ".") \
X(RENDER_FORMAT, renderFormat, TYPE_STRING, "{}.png") \
X(RENDER_IS_USE_ANIMATION_BOUNDS, renderIsUseAnimationBounds, TYPE_BOOL, true) \
X(RENDER_IS_TRANSPARENT, renderIsTransparent, TYPE_BOOL, true) \
X(RENDER_SCALE, renderScale, TYPE_FLOAT, 1.0f) \
X(RENDER_FFMPEG_PATH, renderFFmpegPath, TYPE_STRING, SETTINGS_RENDER_FFMPEG_PATH_VALUE_DEFAULT)
#define SETTINGS_TYPES \
X(INT, int) \
X(BOOL, bool) \
X(FLOAT, float) \
X(STRING, std::string) \
X(IVEC2, glm::ivec2) \
X(IVEC2_WH, glm::ivec2) \
X(VEC2, glm::vec2) \
X(VEC2_WH, glm::vec2) \
X(VEC3, glm::vec3) \
X(VEC4, glm::vec4)
#define X(symbol, name, type, ...) const inline DATATYPE_TO_CTYPE(type) SETTINGS_##symbol##_DEFAULT = __VA_ARGS__;
SETTINGS_LIST
enum Type
{
#define X(name, type) name,
SETTINGS_TYPES
#undef X
};
#define X(name, type) using TYPE_##name = type;
SETTINGS_TYPES
#undef X
struct Settings {
#define X(symbol, name, type, ...) DATATYPE_TO_CTYPE(type) name = SETTINGS_##symbol##_DEFAULT;
SETTINGS_LIST
#define SETTINGS_MEMBERS \
/* Symbol / Name / String / Type / Default */ \
X(WINDOW_SIZE, windowSize, "Window Size", IVEC2_WH, {1600, 900}) \
X(IS_VSYNC, isVsync, "Vsync", BOOL, true) \
X(DISPLAY_SCALE, displayScale, "Display Scale", FLOAT, 1.0f) \
\
X(VIEW_ZOOM_STEP, viewZoomStep, "Zoom Step", FLOAT, 50.0f) \
\
X(PLAYBACK_IS_LOOP, playbackIsLoop, "Loop", BOOL, true) \
X(PLAYBACK_IS_CLAMP_PLAYHEAD, playbackIsClampPlayhead, "Clamp Playhead", BOOL, true) \
\
X(CHANGE_IS_CROP, changeIsCrop, "##Is Crop", BOOL, false) \
X(CHANGE_IS_SIZE, changeIsSize, "##Is Size", BOOL, false) \
X(CHANGE_IS_POSITION, changeIsPosition, "##Is Position", BOOL, false) \
X(CHANGE_IS_PIVOT, changeIsPivot, "##Is Pivot", BOOL, false) \
X(CHANGE_IS_SCALE, changeIsScale, "##Is Scale", BOOL, false) \
X(CHANGE_IS_ROTATION, changeIsRotation, "##Is Rotation", BOOL, false) \
X(CHANGE_IS_DELAY, changeIsDelay, "##Is Delay", BOOL, false) \
X(CHANGE_IS_TINT, changeIsTint, "##Is Tint", BOOL, false) \
X(CHANGE_IS_COLOR_OFFSET, changeIsColorOffset, "##Is Color Offset", BOOL, false) \
X(CHANGE_IS_VISIBLE_SET, changeIsVisibleSet, "##Is Visible", BOOL, false) \
X(CHANGE_IS_INTERPOLATED_SET, changeIsInterpolatedSet, "##Is Interpolated", BOOL, false) \
X(CHANGE_IS_FROM_SELECTED_FRAME, changeIsFromSelectedFrame, "From Selected Frame", BOOL, false) \
X(CHANGE_CROP, changeCrop, "Crop", VEC2, {}) \
X(CHANGE_SIZE, changeSize, "Size", VEC2, {}) \
X(CHANGE_POSITION, changePosition, "Position", VEC2, {}) \
X(CHANGE_PIVOT, changePivot, "Pivot", VEC2, {}) \
X(CHANGE_SCALE, changeScale, "Scale", VEC2, {}) \
X(CHANGE_ROTATION, changeRotation, "Rotation", FLOAT, 0.0f) \
X(CHANGE_DELAY, changeDelay, "Delay", INT, 0) \
X(CHANGE_TINT, changeTint, "Tint", VEC4, {}) \
X(CHANGE_COLOR_OFFSET, changeColorOffset, "Color Offset", VEC3, {}) \
X(CHANGE_IS_VISIBLE, changeIsVisible, "Visible", BOOL, false) \
X(CHANGE_IS_INTERPOLATED, changeIsInterpolated, "Interpolated", BOOL, false) \
X(CHANGE_NUMBER_FRAMES, changeNumberFrames, "Frame Count", INT, 1) \
\
X(SCALE_VALUE, scaleValue, "Scale", FLOAT, 1.0f) \
\
X(PREVIEW_IS_AXES, previewIsAxes, "Axes", BOOL, true) \
X(PREVIEW_IS_GRID, previewIsGrid, "Grid", BOOL, true) \
X(PREVIEW_IS_ROOT_TRANSFORM, previewIsRootTransform, "Root Transform", BOOL, true) \
X(PREVIEW_IS_PIVOTS, previewIsPivots, "Pivots", BOOL, false) \
X(PREVIEW_IS_ICONS, previewIsIcons, "Icons", BOOL, true) \
X(PREVIEW_IS_BORDER, previewIsBorder, "Border", BOOL, false) \
X(PREVIEW_IS_ALT_ICONS, previewIsAltIcons, "Alt Icons", BOOL, false) \
X(PREVIEW_OVERLAY_TRANSPARENCY, previewOverlayTransparency, "Alpha", FLOAT, 255) \
X(PREVIEW_ZOOM, previewZoom, "Zoom", FLOAT, 200.0f) \
X(PREVIEW_PAN, previewPan, "Pan", VEC2, {}) \
X(PREVIEW_GRID_SIZE, previewGridSize, "Size", IVEC2, {32, 32}) \
X(PREVIEW_GRID_OFFSET, previewGridOffset, "Offset", IVEC2, {}) \
X(PREVIEW_GRID_COLOR, previewGridColor, "Color", VEC4, {1.0f, 1.0f, 1.0f, 0.125f}) \
X(PREVIEW_AXES_COLOR, previewAxesColor, "Color", VEC4, {1.0f, 1.0f, 1.0f, 0.125f}) \
X(PREVIEW_BACKGROUND_COLOR, previewBackgroundColor, "Background Color", VEC4, {0.113f, 0.184f, 0.286f, 1.0f}) \
\
X(PROPERTIES_IS_ROUND, propertiesIsRound, "Round", BOOL, false) \
\
X(GENERATE_START_POSITION, generateStartPosition, "Start Position", IVEC2, {}) \
X(GENERATE_SIZE, generateSize, "Size", IVEC2, {64, 64}) \
X(GENERATE_PIVOT, generatePivot, "Pivot", IVEC2, {32, 32}) \
X(GENERATE_ROWS, generateRows, "Rows", INT, 4) \
X(GENERATE_COLUMNS, generateColumns, "Columns", INT, 4) \
X(GENERATE_COUNT, generateCount, "Count", INT, 16) \
X(GENERATE_DELAY, generateDelay, "Delay", INT, 1) \
\
X(EDITOR_IS_GRID, editorIsGrid, "Grid", BOOL, true) \
X(EDITOR_IS_GRID_SNAP, editorIsGridSnap, "Snap", BOOL, true) \
X(EDITOR_IS_BORDER, editorIsBorder, "Border", BOOL, true) \
X(EDITOR_ZOOM, editorZoom, "Zoom", FLOAT, 200.0f) \
X(EDITOR_PAN, editorPan, "Pan", VEC2, {0.0, 0.0}) \
X(EDITOR_GRID_SIZE, editorGridSize, "Size", IVEC2, {32, 32}) \
X(EDITOR_GRID_OFFSET, editorGridOffset, "Offset", IVEC2, {32, 32}) \
X(EDITOR_GRID_COLOR, editorGridColor, "Color", VEC4, {1.0, 1.0, 1.0, 0.125}) \
X(EDITOR_BACKGROUND_COLOR, editorBackgroundColor, "Background Color", VEC4, {0.113, 0.184, 0.286, 1.0}) \
\
X(MERGE_TYPE, mergeType, "Type", INT, 0) \
X(MERGE_IS_DELETE_ANIMATIONS_AFTER, mergeIsDeleteAnimationsAfter, "Delete Animations After", BOOL, false) \
\
X(BAKE_INTERVAL, bakeInterval, "Interval", INT, 1) \
X(BAKE_IS_ROUND_SCALE, bakeIsRoundScale, "Round Scale", BOOL, true) \
X(BAKE_IS_ROUND_ROTATION, bakeIsRoundRotation, "Round Rotation", BOOL, true) \
\
X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, "Add Item Type", INT, anm2::LAYER) \
X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, "##Show Unused", BOOL, true) \
\
X(ONIONSKIN_IS_ENABLED, onionskinIsEnabled, "Enabled", BOOL, false) \
X(ONIONSKIN_DRAW_ORDER, onionskinDrawOrder, "Draw Order", INT, 0) \
X(ONIONSKIN_BEFORE_COUNT, onionskinBeforeCount, "Frames", INT, 0) \
X(ONIONSKIN_AFTER_COUNT, onionskinAfterCount, "Frames", INT, 0) \
X(ONIONSKIN_BEFORE_COLOR, onionskinBeforeColor, "Color", VEC3, types::color::RED) \
X(ONIONSKIN_AFTER_COLOR, onionskinAfterColor, "Color", VEC3, types::color::BLUE) \
\
X(TOOL, tool, "##Tool", INT, 0) \
X(TOOL_COLOR, toolColor, "##Color", VEC4, {1.0, 1.0, 1.0, 1.0}) \
\
X(RENDER_TYPE, renderType, "Output", INT, 0) \
X(RENDER_PATH, renderPath, "Path", STRING, ".") \
X(RENDER_FORMAT, renderFormat, "Format", STRING, "{}.png") \
X(RENDER_IS_USE_ANIMATION_BOUNDS, renderIsUseAnimationBounds, "Use Animation Bounds", BOOL, true) \
X(RENDER_IS_TRANSPARENT, renderIsTransparent, "Transparent", BOOL, true) \
X(RENDER_SCALE, renderScale, "Scale", FLOAT, 1.0f) \
X(RENDER_FFMPEG_PATH, renderFFmpegPath, "FFmpeg Path", STRING, FFMPEG_PATH_DEFAULT)
#define SETTINGS_SHORTCUTS \
/* Symbol / Name / String / Type / Default */ \
X(SHORTCUT_CENTER_VIEW, shortcutCenterView, "Center View", STRING, "Home") \
X(SHORTCUT_FIT, shortcutFit, "Fit", STRING, "F") \
X(SHORTCUT_ZOOM_IN, shortcutZoomIn, "Zoom In", STRING, "Ctrl++") \
X(SHORTCUT_ZOOM_OUT, shortcutZoomOut, "Zoom Out", STRING, "Ctrl+-") \
X(SHORTCUT_PLAY_PAUSE, shortcutPlayPause, "Play/Pause", STRING, "Space") \
X(SHORTCUT_ONIONSKIN, shortcutOnionskin, "Onionskin", STRING, "O") \
X(SHORTCUT_NEW, shortcutNew, "New", STRING, "Ctrl+N") \
X(SHORTCUT_OPEN, shortcutOpen, "Open", STRING, "Ctrl+O") \
X(SHORTCUT_CLOSE, shortcutClose, "Close", STRING, "Ctrl+W") \
X(SHORTCUT_SAVE, shortcutSave, "Save", STRING, "Ctrl+S") \
X(SHORTCUT_SAVE_AS, shortcutSaveAs, "Save As", STRING, "Ctrl+Shift+S") \
X(SHORTCUT_EXIT, shortcutExit, "Exit", STRING, "Alt+F4") \
X(SHORTCUT_SHORTEN_FRAME, shortcutShortenFrame, "Shorten Frame", STRING, "F4") \
X(SHORTCUT_EXTEND_FRAME, shortcutExtendFrame, "Extend Frame", STRING, "F5") \
X(SHORTCUT_INSERT_FRAME, shortcutInsertFrame, "Insert Frame", STRING, "F6") \
X(SHORTCUT_PREVIOUS_FRAME, shortcutPreviousFrame, "Previous Frame", STRING, "Comma") \
X(SHORTCUT_NEXT_FRAME, shortcutNextFrame, "Next Frame", STRING, "Period") \
X(SHORTCUT_PAN, shortcutPan, "Pan", STRING, "P") \
X(SHORTCUT_MOVE, shortcutMove, "Move", STRING, "V") \
X(SHORTCUT_ROTATE, shortcutRotate, "Rotate", STRING, "R") \
X(SHORTCUT_SCALE, shortcutScale, "Scale", STRING, "S") \
X(SHORTCUT_CROP, shortcutCrop, "Crop", STRING, "C") \
X(SHORTCUT_DRAW, shortcutDraw, "Draw", STRING, "B") \
X(SHORTCUT_ERASE, shortcutErase, "Erase", STRING, "E") \
X(SHORTCUT_COLOR_PICKER, shortcutColorPicker, "Color Picker", STRING, "I") \
X(SHORTCUT_UNDO, shortcutUndo, "Undo", STRING, "Ctrl+Z") \
X(SHORTCUT_REDO, shortcutRedo, "Redo", STRING, "Ctrl+Shift+Z") \
X(SHORTCUT_COLOR, shortcutColor, "Color", STRING, "X") \
X(SHORTCUT_COPY, shortcutCopy, "Copy", STRING, "Ctrl+C") \
X(SHORTCUT_CUT, shortcutCut, "Cut", STRING, "Ctrl+X") \
X(SHORTCUT_ADD, shortcutAdd, "Add", STRING, "Insert") \
X(SHORTCUT_REMOVE, shortcutRemove, "Remove", STRING, "Delete") \
X(SHORTCUT_DUPLICATE, shortcutDuplicate, "Duplicate", STRING, "Ctrl+J") \
X(SHORTCUT_DEFAULT, shortcutDefault, "Default", STRING, "Home") \
X(SHORTCUT_MERGE, shortcutMerge, "Merge", STRING, "Ctrl+E") \
X(SHORTCUT_PASTE, shortcutPaste, "Paste", STRING, "Ctrl+V") \
X(SHORTCUT_SELECT_ALL, shortcutSelectAll, "Select All", STRING, "Ctrl+A") \
X(SHORTCUT_SELECT_NONE, shortcutSelectNone, "Select None", STRING, "Escape")
#define SETTINGS_WINDOWS \
/* Symbol / Name / String / Type / Default */ \
X(WINDOW_ANIMATIONS, windowIsAnimations, "Animations", BOOL, true) \
X(WINDOW_ANIMATION_PREVIEW, windowIsAnimationPreview, "Animation Preview", BOOL, true) \
X(WINDOW_EVENTS, windowIsEvents, "Events", BOOL, true) \
X(WINDOW_FRAME_PROPERTIES, windowIsFrameProperties, "Frame Properties", BOOL, true) \
X(WINDOW_LAYERS, windowIsLayers, "Layers", BOOL, true) \
X(WINDOW_NULLS, windowIsNulls, "Nulls", BOOL, true) \
X(WINDOW_ONIONSKIN, windowIsOnionskin, "Onionskin", BOOL, true) \
X(WINDOW_PREVIEW, windowIsSpritesheets, "Spritesheets", BOOL, true) \
X(WINDOW_SPRITESHEET_EDITOR, windowIsSpritesheetEditor, "Spritesheet Editor", BOOL, true) \
X(WINDOW_TIMELINE, windowIsTimeline, "Timeline", BOOL, true) \
X(WINDOW_TOOLS, windowIsTools, "Tools", BOOL, true)
class Settings
{
public:
#define X(symbol, name, string, type, ...) TYPE_##type name = __VA_ARGS__;
SETTINGS_MEMBERS SETTINGS_SHORTCUTS SETTINGS_WINDOWS
#undef X
};
struct SettingsEntry {
std::string key;
DataType type;
int offset;
};
Settings();
const inline SettingsEntry SETTINGS_ENTRIES[] = {
#define X(symbol, name, type, ...) {#name, type, offsetof(Settings, name)},
SETTINGS_LIST
Settings(const std::string& path);
void save(const std::string& path, const std::string& imguiData);
};
enum ShortcutType
{
#define X(symbol, name, string, type, ...) symbol,
SETTINGS_SHORTCUTS
#undef X
};
SHORTCUT_COUNT
};
constexpr int SETTINGS_COUNT = (int)std::size(SETTINGS_ENTRIES);
#define HOTKEY_LIST \
X(NONE, "None") \
X(CENTER_VIEW, "Center View") \
X(FIT, "Fit") \
X(ZOOM_IN, "Zoom In") \
X(ZOOM_OUT, "Zoom Out") \
X(PLAY_PAUSE, "Play/Pause") \
X(ONIONSKIN, "Onionskin") \
X(NEW, "New") \
X(OPEN, "Open") \
X(SAVE, "Save") \
X(SAVE_AS, "Save As") \
X(EXIT, "Exit") \
X(SHORTEN_FRAME, "Shorten Frame") \
X(EXTEND_FRAME, "Extend Frame") \
X(INSERT_FRAME, "Insert Frame") \
X(PREVIOUS_FRAME, "Previous Frame") \
X(NEXT_FRAME, "Next Frame") \
X(PAN, "Pan") \
X(MOVE, "Move") \
X(ROTATE, "Rotate") \
X(SCALE, "Scale") \
X(CROP, "Crop") \
X(DRAW, "Draw") \
X(ERASE, "Erase") \
X(COLOR_PICKER, "Color Picker") \
X(UNDO, "Undo") \
X(REDO, "Redo") \
X(COPY, "Copy") \
X(CUT, "Cut") \
X(PASTE, "Paste") \
X(SELECT_ALL, "Select All") \
X(SELECT_NONE, "Select None")
typedef enum {
#define X(name, str) HOTKEY_##name,
HOTKEY_LIST
constexpr const char* SHORTCUT_STRINGS[] = {
#define X(symbol, name, string, type, ...) string,
SETTINGS_SHORTCUTS
#undef X
HOTKEY_COUNT
} HotkeyType;
};
const inline char* HOTKEY_STRINGS[] = {
#define X(name, str) str,
HOTKEY_LIST
using ShortcutMember = std::string Settings::*;
constexpr ShortcutMember SHORTCUT_MEMBERS[] = {
#define X(symbol, name, string, type, ...) &Settings::name,
SETTINGS_SHORTCUTS
#undef X
};
};
using HotkeyMember = std::string Settings::*;
enum WindowType
{
#define X(symbol, name, string, type, ...) symbol,
SETTINGS_WINDOWS
#undef X
WINDOW_COUNT
};
const inline HotkeyMember SETTINGS_HOTKEY_MEMBERS[HOTKEY_COUNT] = {nullptr,
&Settings::hotkeyCenterView,
&Settings::hotkeyFit,
&Settings::hotkeyZoomIn,
&Settings::hotkeyZoomOut,
&Settings::hotkeyPlayPause,
&Settings::hotkeyOnionskin,
&Settings::hotkeyNew,
&Settings::hotkeyOpen,
&Settings::hotkeySave,
&Settings::hotkeySaveAs,
&Settings::hotkeyExit,
&Settings::hotkeyShortenFrame,
&Settings::hotkeyExtendFrame,
&Settings::hotkeyInsertFrame,
&Settings::hotkeyPreviousFrame,
&Settings::hotkeyNextFrame,
&Settings::hotkeyPan,
&Settings::hotkeyMove,
&Settings::hotkeyRotate,
&Settings::hotkeyScale,
&Settings::hotkeyCrop,
&Settings::hotkeyDraw,
&Settings::hotkeyErase,
&Settings::hotkeyColorPicker,
&Settings::hotkeyUndo,
&Settings::hotkeyRedo,
&Settings::hotkeyCopy,
&Settings::hotkeyCut,
&Settings::hotkeyPaste,
&Settings::hotkeySelectAll,
&Settings::hotkeySelectNone};
constexpr const char* WINDOW_STRINGS[] = {
#define X(symbol, name, string, type, ...) string,
SETTINGS_WINDOWS
#undef X
};
const inline std::string SETTINGS_IMGUI_DEFAULT = R"(
# Dear ImGui
[Window][## Window]
Pos=0,32
Size=1600,868
Collapsed=0
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][Tools]
Pos=8,40
Size=38,516
Collapsed=0
DockId=0x0000000B,0
[Window][Animations]
Pos=1289,307
Size=303,249
Collapsed=0
DockId=0x0000000A,0
[Window][Events]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,2
[Window][Spritesheets]
Pos=1289,40
Size=303,265
Collapsed=0
DockId=0x00000009,0
[Window][Animation Preview]
Pos=48,40
Size=907,516
Collapsed=0
DockId=0x0000000C,0
[Window][Spritesheet Editor]
Pos=48,40
Size=907,516
Collapsed=0
DockId=0x0000000C,1
[Window][Timeline]
Pos=8,558
Size=1584,334
Collapsed=0
DockId=0x00000004,0
[Window][Frame Properties]
Pos=957,40
Size=330,222
Collapsed=0
DockId=0x00000007,0
[Window][Onionskin]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,3
[Window][Layers]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,0
[Window][Nulls]
Pos=957,264
Size=330,292
Collapsed=0
DockId=0x00000008,1
[Docking][Data]
DockSpace ID=0xFC02A410 Window=0x0E46F4F7 Pos=8,40 Size=1584,852 Split=Y
DockNode ID=0x00000003 Parent=0xFC02A410 SizeRef=1902,680 Split=X
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=1017,1016 Split=X Selected=0x024430EF
DockNode ID=0x00000005 Parent=0x00000001 SizeRef=1264,654 Split=X Selected=0x024430EF
DockNode ID=0x0000000B Parent=0x00000005 SizeRef=38,654 Selected=0x18A5FDB9
DockNode ID=0x0000000C Parent=0x00000005 SizeRef=1224,654 CentralNode=1 Selected=0x024430EF
DockNode ID=0x00000006 Parent=0x00000001 SizeRef=330,654 Split=Y Selected=0x754E368F
DockNode ID=0x00000007 Parent=0x00000006 SizeRef=631,293 Selected=0x754E368F
DockNode ID=0x00000008 Parent=0x00000006 SizeRef=631,385 Selected=0xCD8384B1
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=303,1016 Split=Y Selected=0x4EFD0020
DockNode ID=0x00000009 Parent=0x00000002 SizeRef=634,349 Selected=0x4EFD0020
DockNode ID=0x0000000A Parent=0x00000002 SizeRef=634,329 Selected=0xC1986EE2
DockNode ID=0x00000004 Parent=0xFC02A410 SizeRef=1902,334 Selected=0x4F89F0DC
)";
void settings_save(Settings* self);
void settings_init(Settings* self);
std::string settings_path_get(void);
using WindowMember = bool Settings::*;
static constexpr WindowMember WINDOW_MEMBERS[] = {
#define X(symbol, name, string, type, ...) &Settings::name,
SETTINGS_WINDOWS
#undef X
};
}

View File

@@ -1,49 +1,74 @@
#include "shader.h"
static bool _shader_compile(GLuint* self, const std::string& text) {
int isCompile;
const GLchar* source = text.c_str();
#include "log.h"
glShaderSource(*self, 1, &source, nullptr);
glCompileShader(*self);
glGetShaderiv(*self, GL_COMPILE_STATUS, &isCompile);
using namespace anm2ed::log;
if (!isCompile) {
std::string compileLog(SHADER_INFO_LOG_MAX, '\0');
glGetShaderInfoLog(*self, SHADER_INFO_LOG_MAX, nullptr, compileLog.data());
log_error(std::format(SHADER_INIT_ERROR, *self, compileLog.c_str()));
return false;
namespace anm2ed::shader
{
Shader::Shader() = default;
Shader::Shader(const char* vertex, const char* fragment)
{
id = glCreateProgram();
auto compile = [&](const GLuint& id, const char* text)
{
int isCompile{};
glShaderSource(id, 1, &text, nullptr);
glCompileShader(id);
glGetShaderiv(id, GL_COMPILE_STATUS, &isCompile);
if (!isCompile)
{
std::string compileLog(255, '\0');
glGetShaderInfoLog(id, 255, nullptr, compileLog.data());
logger.error(std::format("Unable to compile shader {}: {}", id, compileLog.c_str()));
return false;
}
return true;
};
auto vertexHandle = glCreateShader(GL_VERTEX_SHADER);
auto fragmentHandle = glCreateShader(GL_FRAGMENT_SHADER);
if (!(compile(vertexHandle, vertex) && compile(fragmentHandle, fragment))) return;
glAttachShader(id, vertexHandle);
glAttachShader(id, fragmentHandle);
glLinkProgram(id);
auto isLinked = GL_FALSE;
glGetProgramiv(id, GL_LINK_STATUS, &isLinked);
if (!isLinked)
{
glDeleteProgram(id);
id = 0;
}
glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle);
}
return true;
Shader& Shader::operator=(Shader&& other) noexcept
{
if (this != &other)
{
if (is_valid()) glDeleteProgram(id);
id = other.id;
other.id = 0;
}
return *this;
}
Shader::~Shader()
{
if (is_valid()) glDeleteProgram(id);
}
bool Shader::is_valid() const
{
return id != 0;
}
}
bool shader_init(GLuint* self, const std::string& vertex, const std::string& fragment) {
GLuint vertexHandle;
GLuint fragmentHandle;
vertexHandle = glCreateShader(GL_VERTEX_SHADER);
fragmentHandle = glCreateShader(GL_FRAGMENT_SHADER);
if (!_shader_compile(&vertexHandle, vertex) || !_shader_compile(&fragmentHandle, fragment))
return false;
*self = glCreateProgram();
glAttachShader(*self, vertexHandle);
glAttachShader(*self, fragmentHandle);
glLinkProgram(*self);
glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle);
return true;
}
void shader_free(GLuint* self) {
if (*self == GL_ID_NONE)
return;
glDeleteProgram(*self);
}

View File

@@ -1,9 +1,156 @@
#pragma once
#include "log.h"
#include <glad/glad.h>
#define SHADER_INFO_LOG_MAX 0xFF
#define SHADER_INIT_ERROR "Failed to initialize shader {}:\n{}"
namespace anm2ed::shader
{
struct Info
{
const char* vertex;
const char* fragment;
};
bool shader_init(GLuint* self, const std::string& vertex, const std::string& fragment);
void shader_free(GLuint* self);
constexpr auto VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 i_uv_out;
uniform mat4 u_transform;
void main()
{
i_uv_out = i_uv;
gl_Position = u_transform * vec4(i_position, 0.0, 1.0);
}
)";
constexpr auto AXIS_VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position; // full screen line segment: -1..1
uniform vec2 u_origin_ndc; // world origin in NDC
uniform int u_axis; // 0 = X axis, 1 = Y axis
void main()
{
vec2 pos = (u_axis == 0)
? vec2(i_position.x, u_origin_ndc.y) // horizontal line across screen
: vec2(u_origin_ndc.x, i_position.x); // vertical line across screen;
gl_Position = vec4(pos, 0.0, 1.0);
}
)";
constexpr auto GRID_VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 i_uv_out;
void main() {
i_uv_out = i_position;
gl_Position = vec4(i_position, 0.0, 1.0);
}
)";
constexpr auto FRAGMENT = R"(
#version 330 core
out vec4 o_fragColor;
uniform vec4 u_color;
void main()
{
o_fragColor = u_color;
}
)";
constexpr auto TEXTURE_FRAGMENT = R"(
#version 330 core
in vec2 i_uv_out;
uniform sampler2D u_texture;
uniform vec4 u_tint;
uniform vec3 u_color_offset;
out vec4 o_fragColor;
void main()
{
vec4 texColor = texture(u_texture, i_uv_out);
texColor *= u_tint;
texColor.rgb += u_color_offset;
o_fragColor = texColor;
}
)";
constexpr auto GRID_FRAGMENT = R"(
#version 330 core
in vec2 i_uv_out;
uniform vec2 u_view_size;
uniform vec2 u_pan;
uniform float u_zoom;
uniform vec2 u_size;
uniform vec2 u_offset;
uniform vec4 u_color;
out vec4 o_fragColor;
void main()
{
vec2 viewSize = max(u_view_size, vec2(1.0));
float zoom = max(u_zoom, 1e-6);
vec2 pan = u_pan;
vec2 world = (i_uv_out - (2.0 * pan / viewSize)) * (viewSize / (2.0 * zoom));
vec2 cell = max(u_size, vec2(1.0));
vec2 grid = (world - u_offset) / cell;
vec2 d = abs(fract(grid) - 0.5);
float distance = min(d.x, d.y);
float fw = min(fwidth(grid.x), fwidth(grid.y));
float alpha = 1.0 - smoothstep(0.0, fw, distance);
if (alpha <= 0.0)
discard;
o_fragColor = vec4(u_color.rgb, u_color.a * alpha);
}
)";
constexpr auto UNIFORM_AXIS = "u_axis";
constexpr auto UNIFORM_COLOR = "u_color";
constexpr auto UNIFORM_TRANSFORM = "u_transform";
constexpr auto UNIFORM_TINT = "u_tint";
constexpr auto UNIFORM_COLOR_OFFSET = "u_color_offset";
constexpr auto UNIFORM_OFFSET = "u_offset";
constexpr auto UNIFORM_ORIGIN_NDC = "u_origin_ndc";
constexpr auto UNIFORM_SIZE = "u_size";
constexpr auto UNIFORM_MODEL = "u_model";
constexpr auto UNIFORM_RECT_SIZE = "u_rect_size";
constexpr auto UNIFORM_TEXTURE = "u_texture";
constexpr auto UNIFORM_VIEW_SIZE = "u_view_size";
constexpr auto UNIFORM_PAN = "u_pan";
constexpr auto UNIFORM_ZOOM = "u_zoom";
enum Type
{
LINE,
TEXTURE,
AXIS,
GRID,
COUNT
};
const Info SHADERS[COUNT] = {
{VERTEX, FRAGMENT}, {VERTEX, TEXTURE_FRAGMENT}, {AXIS_VERTEX, FRAGMENT}, {GRID_VERTEX, GRID_FRAGMENT}};
class Shader
{
public:
GLuint id{};
Shader();
Shader(const char* vertex, const char* fragment);
Shader& operator=(Shader&& other) noexcept;
~Shader();
bool is_valid() const;
};
}

View File

@@ -1,67 +0,0 @@
#include "snapshots.h"
static void _snapshot_stack_push(SnapshotStack* stack, Snapshot* snapshot) {
if (stack->top >= SNAPSHOT_STACK_MAX) {
for (int i = 0; i < SNAPSHOT_STACK_MAX - 1; i++)
stack->snapshots[i] = stack->snapshots[i + 1];
stack->top = SNAPSHOT_STACK_MAX - 1;
}
stack->snapshots[stack->top++] = *snapshot;
}
static Snapshot* _snapshot_stack_pop(SnapshotStack* stack) {
if (stack->top == 0)
return nullptr;
return &stack->snapshots[--stack->top];
}
static void _snapshot_set(Snapshots* self, Snapshot* snapshot) {
if (!snapshot)
return;
*self->anm2 = snapshot->anm2;
*self->reference = snapshot->reference;
self->preview->time = snapshot->time;
self->action = snapshot->action;
anm2_spritesheet_texture_pixels_upload(self->anm2);
}
Snapshot snapshot_get(Snapshots* self) {
Snapshot snapshot = {*self->anm2, *self->reference, self->preview->time, self->action};
anm2_spritesheet_texture_pixels_download(&snapshot.anm2);
return snapshot;
}
void snapshots_init(Snapshots* self, Anm2* anm2, Anm2Reference* reference, Preview* preview) {
self->anm2 = anm2;
self->reference = reference;
self->preview = preview;
}
void snapshots_reset(Snapshots* self) {
self->undoStack = SnapshotStack{};
self->redoStack = SnapshotStack{};
self->action.clear();
}
void snapshots_undo_push(Snapshots* self, Snapshot* snapshot) {
self->redoStack.top = 0;
_snapshot_stack_push(&self->undoStack, snapshot);
}
void snapshots_undo(Snapshots* self) {
if (Snapshot* snapshot = _snapshot_stack_pop(&self->undoStack)) {
Snapshot current = snapshot_get(self);
_snapshot_stack_push(&self->redoStack, &current);
_snapshot_set(self, snapshot);
}
}
void snapshots_redo(Snapshots* self) {
if (Snapshot* snapshot = _snapshot_stack_pop(&self->redoStack)) {
Snapshot current = snapshot_get(self);
_snapshot_stack_push(&self->undoStack, &current);
_snapshot_set(self, snapshot);
}
}

View File

@@ -1,37 +0,0 @@
#pragma once
#include "anm2.h"
#include "preview.h"
#define SNAPSHOT_STACK_MAX 100
#define SNAPSHOT_ACTION "Action"
struct Snapshot {
Anm2 anm2;
Anm2Reference reference;
float time = 0.0f;
std::string action = SNAPSHOT_ACTION;
};
struct SnapshotStack {
Snapshot snapshots[SNAPSHOT_STACK_MAX];
int top = 0;
bool empty() const { return top == 0; }
};
struct Snapshots {
Anm2* anm2 = nullptr;
Preview* preview = nullptr;
Anm2Reference* reference = nullptr;
std::string action = SNAPSHOT_ACTION;
SnapshotStack undoStack;
SnapshotStack redoStack;
};
void snapshots_undo_push(Snapshots* self, Snapshot* snapshot);
void snapshots_init(Snapshots* self, Anm2* anm2, Anm2Reference* reference, Preview* preview);
void snapshots_undo(Snapshots* self);
void snapshots_redo(Snapshots* self);
void snapshots_reset(Snapshots* self);
Snapshot snapshot_get(Snapshots* self);

128
src/spritesheet_editor.cpp Normal file
View File

@@ -0,0 +1,128 @@
#include "spritesheet_editor.h"
#include "imgui.h"
#include "math.h"
#include "tool.h"
#include "types.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::settings;
using namespace anm2ed::canvas;
using namespace anm2ed::resources;
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::spritesheet_editor
{
SpritesheetEditor::SpritesheetEditor() : Canvas(vec2())
{
}
void SpritesheetEditor::update(DocumentManager& manager, Settings& settings, Resources& resources)
{
auto& document = *manager.get();
auto& pan = document.editorPan;
auto& zoom = document.editorZoom;
auto& backgroundColor = settings.editorBackgroundColor;
auto& gridColor = settings.editorGridColor;
auto& gridSize = settings.editorGridSize;
auto& gridOffset = settings.editorGridOffset;
auto& isGrid = settings.editorIsGrid;
auto& zoomStep = settings.viewZoomStep;
auto& isBorder = settings.editorIsBorder;
auto spritesheet = document.spritesheet_get();
auto& tool = settings.tool;
auto& shaderGrid = resources.shaders[shader::GRID];
auto& shaderTexture = resources.shaders[shader::TEXTURE];
auto& lineShader = resources.shaders[shader::LINE];
if (ImGui::Begin("Spritesheet Editor", &settings.windowIsSpritesheetEditor))
{
auto childSize = ImVec2(imgui::row_widget_width_get(3),
(ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2));
if (ImGui::BeginChild("##Grid Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::Checkbox("Grid", &isGrid);
ImGui::SameLine();
ImGui::ColorEdit4("Color", value_ptr(gridColor), ImGuiColorEditFlags_NoInputs);
ImGui::InputInt2("Size", value_ptr(gridSize));
ImGui::InputInt2("Offset", value_ptr(gridOffset));
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::InputFloat("Zoom", &zoom, zoomStep, zoomStep, "%.0f%%");
auto widgetSize = ImVec2(imgui::row_widget_width_get(2), 0);
if (ImGui::Button("Center View", widgetSize)) pan = vec2();
ImGui::SameLine();
ImGui::Button("Fit", widgetSize);
ImGui::TextUnformatted(std::format(POSITION_FORMAT, (int)mousePos.x, (int)mousePos.y).c_str());
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs);
ImGui::Checkbox("Border", &isBorder);
}
ImGui::EndChild();
auto cursorScreenPos = ImGui::GetCursorScreenPos();
size_set(to_vec2(ImGui::GetContentRegionAvail()));
bind();
viewport_set();
clear(backgroundColor);
if (spritesheet)
{
auto& texture = spritesheet->texture;
auto transform = transform_get(zoom, pan) * math::quad_model_get(texture.size);
texture_render(shaderTexture, texture.id, transform);
if (isBorder) rect_render(lineShader, transform);
}
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
unbind();
ImGui::Image(texture, to_imvec2(size));
if (ImGui::IsItemHovered())
{
ImGui::SetKeyboardFocusHere(-1);
mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos));
auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
auto mouseDelta = ImGui::GetIO().MouseDelta;
auto mouseWheel = ImGui::GetIO().MouseWheel;
auto isZoomIn = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomIn));
auto isZoomOut = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomOut));
if ((tool == tool::PAN && isMouseDown) || isMouseMiddleDown) pan += vec2(mouseDelta.x, mouseDelta.y);
switch (tool)
{
default:
break;
}
if (mouseWheel != 0 || isZoomIn || isZoomOut)
zoom_set(zoom, pan, mousePos, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep);
}
}
ImGui::End();
}
}

19
src/spritesheet_editor.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "canvas.h"
#include "document_manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::spritesheet_editor
{
class SpritesheetEditor : public canvas::Canvas
{
glm::vec2 mousePos{};
public:
SpritesheetEditor();
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
};
}

250
src/spritesheets.cpp Normal file
View File

@@ -0,0 +1,250 @@
#include "spritesheets.h"
#include <ranges>
#include "imgui.h"
#include "toast.h"
#include "types.h"
using namespace anm2ed::anm2;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::dialog;
using namespace anm2ed::document_manager;
using namespace anm2ed::types;
using namespace anm2ed::toast;
using namespace glm;
namespace anm2ed::spritesheets
{
void Spritesheets::update(DocumentManager& manager, Settings& settings, Resources& resources, Dialog& dialog)
{
if (ImGui::Begin("Spritesheets", &settings.windowIsSpritesheets))
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& selection = document.selectedSpritesheets;
auto style = ImGui::GetStyle();
static ImGuiSelectionExternalStorage storage{};
storage.UserData = &selection;
storage.AdapterSetItemSelected = imgui::external_storage_set;
auto childSize = imgui::size_with_footer_get(2);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::BeginChild("##Spritesheets Child", childSize, ImGuiChildFlags_Borders))
{
auto spritesheetChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 4);
ImGuiMultiSelectIO* io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(),
anm2.content.spritesheets.size());
storage.ApplyRequests(io);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
for (auto& [id, spritesheet] : anm2.content.spritesheets)
{
ImGui::PushID(id);
if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders))
{
auto isSelected = selection.contains(id);
auto isReferenced = id == document.referenceSpritesheet;
auto cursorPos = ImGui::GetCursorPos();
auto& texture = spritesheet.texture;
ImGui::SetNextItemSelectionUserData(id);
ImGui::SetNextItemStorageID(id);
if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize))
document.referenceSpritesheet = id;
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
if (ImGui::BeginItemTooltip())
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
auto viewport = ImGui::GetMainViewport();
auto size = texture.size.x * texture.size.y > (viewport->Size.x * viewport->Size.y) * 0.5f
? to_vec2(viewport->Size) * 0.5f
: vec2(texture.size);
auto aspectRatio = (float)texture.size.x / texture.size.y;
if (size.x / size.y > aspectRatio)
size.x = size.y * aspectRatio;
else
size.y = size.x / aspectRatio;
if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(size), ImGuiChildFlags_Borders))
ImGui::Image(texture.id, ImGui::GetContentRegionAvail());
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::SameLine();
if (ImGui::BeginChild(
"##Spritesheet Info Tooltip Child",
ImVec2(ImGui::CalcTextSize(spritesheet.path.c_str()).x + ImGui::GetTextLineHeightWithSpacing(),
0)))
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(spritesheet.path.c_str());
ImGui::PopFont();
ImGui::TextUnformatted(std::format("ID: {}", id).c_str());
ImGui::TextUnformatted(std::format("Size: {} x {}", texture.size.x, texture.size.y).c_str());
}
ImGui::EndChild();
ImGui::EndTooltip();
}
ImGui::PopStyleVar(2);
auto imageSize = to_imvec2(vec2(spritesheetChildSize.y));
auto aspectRatio = (float)texture.size.x / texture.size.y;
if (imageSize.x / imageSize.y > aspectRatio)
imageSize.x = imageSize.y * aspectRatio;
else
imageSize.y = imageSize.x / aspectRatio;
ImGui::SetCursorPos(cursorPos);
ImGui::Image(texture.id, imageSize);
ImGui::SetCursorPos(
ImVec2(spritesheetChildSize.y + style.ItemSpacing.x,
spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::TextUnformatted(std::format("#{} {}", id, spritesheet.path.string()).c_str());
if (isReferenced) ImGui::PopFont();
}
ImGui::EndChild();
ImGui::PopID();
}
io = ImGui::EndMultiSelect();
storage.ApplyRequests(io);
ImGui::PopStyleVar();
}
ImGui::EndChild();
ImGui::PopStyleVar();
auto rowOneWidgetSize = imgui::widget_size_with_row_get(4);
imgui::shortcut(settings.shortcutAdd, true);
if (ImGui::Button("Add", rowOneWidgetSize)) dialog.spritesheet_open();
imgui::set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd);
if (dialog.is_selected_file(dialog::SPRITESHEET_OPEN))
{
manager.spritesheet_add(dialog.path);
dialog.reset();
}
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
{
if (ImGui::Button("Reload", rowOneWidgetSize))
{
for (auto& id : selection)
{
Spritesheet& spritesheet = anm2.content.spritesheets[id];
spritesheet.reload(document.directory_get());
toasts.add(std::format("Reloaded spritesheet #{}: {}", id, spritesheet.path.string()));
}
}
ImGui::SetItemTooltip("Reloads the selected spritesheets.");
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(selection.size() != 1);
{
if (ImGui::Button("Replace", rowOneWidgetSize)) dialog.spritesheet_replace();
ImGui::SetItemTooltip("Replace the selected spritesheet with a new one.");
}
ImGui::EndDisabled();
if (dialog.is_selected_file(dialog::SPRITESHEET_REPLACE))
{
auto& id = *selection.begin();
Spritesheet& spritesheet = anm2.content.spritesheets[id];
spritesheet = Spritesheet(document.directory_get(), dialog.path);
toasts.add(std::format("Replaced spritesheet #{}: {}", id, spritesheet.path.string()));
dialog.reset();
}
ImGui::SameLine();
auto unused = anm2.spritesheets_unused();
ImGui::BeginDisabled(unused.empty());
{
imgui::shortcut(settings.shortcutRemove, true);
if (ImGui::Button("Remove Unused", rowOneWidgetSize))
{
for (auto& id : unused)
{
Spritesheet& spritesheet = anm2.content.spritesheets[id];
toasts.add(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string()));
anm2.spritesheet_remove(id);
}
}
imgui::set_item_tooltip_shortcut("Remove all unused spritesheets (i.e., not used in any layer.).",
settings.shortcutRemove);
}
ImGui::EndDisabled();
auto rowTwoWidgetSize = imgui::widget_size_with_row_get(3);
imgui::shortcut(settings.shortcutSelectAll, true);
ImGui::BeginDisabled(selection.size() == anm2.content.spritesheets.size());
{
if (ImGui::Button("Select All", rowTwoWidgetSize))
for (auto& id : anm2.content.spritesheets | std::views::keys)
selection.insert(id);
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Select all spritesheets.", settings.shortcutSelectAll);
ImGui::SameLine();
imgui::shortcut(settings.shortcutSelectNone, true);
ImGui::BeginDisabled(selection.empty());
{
if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear();
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone);
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
{
if (ImGui::Button("Save", rowTwoWidgetSize))
{
for (auto& id : selection)
{
if (Spritesheet& spritesheet = anm2.content.spritesheets[id]; spritesheet.save(document.directory_get()))
toasts.add(std::format("Saved spritesheet #{}: {}", id, spritesheet.path.string()));
else
toasts.add(std::format("Unable to save spritesheet #{}: {}", id, spritesheet.path.string()));
}
}
}
ImGui::EndDisabled();
ImGui::SetItemTooltip("Save the selected spritesheets.");
}
ImGui::End();
}
}

16
src/spritesheets.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "dialog.h"
#include "document_manager.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::spritesheets
{
class Spritesheets
{
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources, dialog::Dialog& dialog);
};
}

View File

@@ -1,228 +1,101 @@
#include "state.h"
static void _tick(State* self)
#include <imgui/backends/imgui_impl_opengl3.h>
#include <imgui/backends/imgui_impl_sdl3.h>
#include "filesystem.h"
#include "toast.h"
using namespace anm2ed::settings;
using namespace anm2ed::dialog;
using namespace anm2ed::toast;
using namespace anm2ed::types;
namespace anm2ed::state
{
preview_tick(&self->preview);
}
constexpr auto TICK_RATE = 30;
constexpr auto TICK_INTERVAL = (1000 / TICK_RATE);
constexpr auto UPDATE_RATE = 120;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
static void _update(State* self)
{
SDL_GetWindowSize(self->window, &self->settings.windowSize.x, &self->settings.windowSize.y);
imgui_update(&self->imgui);
State::State(SDL_Window*& window, std::vector<std::string>& arguments)
{
dialog = Dialog(window);
if (self->imgui.isQuit)
self->isRunning = false;
}
for (auto argument : arguments)
manager.open(argument);
}
static void _draw(State* self)
{
imgui_draw();
void State::tick(Settings& settings)
{
if (auto document = manager.get())
if (auto animation = document->animation_get())
if (playback.isPlaying)
playback.tick(document->anm2.info.fps, animation->frameNum, animation->isLoop || settings.playbackIsLoop);
}
SDL_GL_SwapWindow(self->window);
}
void State::update(SDL_Window*& window, Settings& settings)
{
SDL_Event event{};
bool sdl_init(State* self, bool isTestMode = false)
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
log_error(std::format(STATE_SDL_INIT_ERROR, SDL_GetError()));
quit(self);
return false;
}
if (!isTestMode) log_info(STATE_SDL_INIT_INFO);
// Todo, when sdl3 mixer is released officially
/*
if ((Mix_Init(STATE_MIX_FLAGS) & mixFlags) != mixFlags)
log_warning(std::format(STATE_MIX_INIT_WARNING, Mix_GetError()));
if
(
Mix_OpenAudioDevice
(
STATE_MIX_SAMPLE_RATE,
STATE_MIX_FORMAT,
STATE_MIX_CHANNELS,
STATE_CHUNK_SIZE,
STATE_MIX_DEVICE,
STATE_MIX_ALLOWED_CHANGES
)
< 0
)
{
log_warning(std::format(STATE_MIX_INIT_WARNING, Mix_GetError()));
Mix_Quit();
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL3_ProcessEvent(&event);
switch (event.type)
{
case SDL_EVENT_DROP_FILE:
if (auto droppedFile = event.drop.data; filesystem::path_is_extension(droppedFile, "anm2"))
manager.open(std::string(droppedFile));
break;
case SDL_EVENT_QUIT:
isQuit = true;
break;
default:
break;
}
}
else
log_info(STATE_MIX_INIT_INFO);
*/
ImGui_ImplSDL3_NewFrame();
ImGui_ImplOpenGL3_NewFrame();
ImGui::NewFrame();
if (isTestMode)
{
self->window = SDL_CreateWindow
(
WINDOW_TITLE,
WINDOW_TEST_MODE_SIZE.x, WINDOW_TEST_MODE_SIZE.y,
WINDOW_TEST_MODE_FLAGS
);
}
else
{
ivec2 windowSize = self->settings.windowSize;
taskbar.update(settings, dialog, manager, isQuit);
documents.update(taskbar, manager, resources);
dockspace.update(taskbar, documents, manager, settings, resources, dialog, playback);
toasts.update();
// Fix for auto-fullscreen on Windows
if (SDL_DisplayID* displayIDs = SDL_GetDisplays(nullptr))
if (displayIDs[0])
if (const SDL_DisplayMode* displayMode = SDL_GetDesktopDisplayMode(displayIDs[0]))
if (windowSize.x == displayMode->w && windowSize.y == displayMode->h)
windowSize -= ivec2(1, 1);
self->window = SDL_CreateWindow
(
WINDOW_TITLE,
windowSize.x,
windowSize.y,
WINDOW_FLAGS
);
}
ImGui::GetStyle().FontScaleMain = settings.displayScale;
SDL_GetWindowSize(window, &settings.windowSize.x, &settings.windowSize.y);
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, STATE_GL_VERSION_MAJOR);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, STATE_GL_VERSION_MINOR);
void State::render(SDL_Window*& window, Settings& settings)
{
glViewport(0, 0, settings.windowSize.x, settings.windowSize.y);
glClear(GL_COLOR_BUFFER_BIT);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
SDL_GL_SetSwapInterval(settings.isVsync);
}
self->glContext = SDL_GL_CreateContext(self->window);
if (!self->glContext)
{
log_error(std::format(STATE_GL_CONTEXT_INIT_ERROR, SDL_GetError()));
quit(self);
return false;
}
void State::loop(SDL_Window*& window, Settings& settings)
{
auto currentTick = SDL_GetTicks();
auto currentUpdate = SDL_GetTicks();
if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
{
log_error(std::format(STATE_GLAD_INIT_ERROR));
quit(self);
return false;
}
if (currentTick - previousTick >= TICK_INTERVAL)
{
tick(settings);
previousTick = currentTick;
}
if (!isTestMode) log_info(std::format(STATE_GL_CONTEXT_INIT_INFO, (const char*)glGetString(GL_VERSION)));
window_vsync_set(self->settings.isVsync);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glLineWidth(STATE_GL_LINE_WIDTH);
glDisable(GL_MULTISAMPLE);
glDisable(GL_DEPTH_TEST);
glDisable(GL_LINE_SMOOTH);
if (currentUpdate - previousUpdate >= UPDATE_INTERVAL)
{
update(window, settings);
render(window, settings);
previousUpdate = currentUpdate;
}
return true;
SDL_Delay(1);
}
}
void init(State* self)
{
log_info(STATE_INIT_INFO);
settings_init(&self->settings);
if (!sdl_init(self)) return;
if (!self->argument.empty())
{
anm2_deserialize(&self->anm2, self->argument);
window_title_from_path_set(self->window, self->argument);
}
else
anm2_new(&self->anm2);
resources_init(&self->resources);
dialog_init(&self->dialog, self->window);
clipboard_init(&self->clipboard, &self->anm2);
preview_init(&self->preview, &self->anm2, &self->reference, &self->resources, &self->settings);
generate_preview_init(&self->generatePreview, &self->anm2, &self->reference, &self->resources, &self->settings);
editor_init(&self->editor, &self->anm2, &self->reference, &self->resources, &self->settings);
snapshots_init(&self->snapshots, &self->anm2, &self->reference, &self->preview);
imgui_init
(
&self->imgui,
&self->dialog,
&self->resources,
&self->anm2,
&self->reference,
&self->editor,
&self->preview,
&self->generatePreview,
&self->settings,
&self->snapshots,
&self->clipboard,
self->window,
&self->glContext
);
}
void loop(State* self)
{
self->tick = SDL_GetTicks();
self->update = self->tick;
while (self->tick > self->lastTick + TICK_DELAY)
{
self->tick = SDL_GetTicks();
if (self->tick - self->lastTick < TICK_DELAY)
SDL_Delay(TICK_DELAY - (self->tick - self->lastTick));
_tick(self);
self->lastTick = self->tick;
}
if (self->settings.isVsync)
{
_update(self);
_draw(self);
}
else
{
while (self->update > self->lastUpdate + UPDATE_DELAY)
{
self->update = SDL_GetTicks();
if (self->update - self->lastUpdate < UPDATE_DELAY)
SDL_Delay(UPDATE_DELAY - (self->update - self->lastUpdate));
_update(self);
_draw(self);
self->lastUpdate = self->update;
}
SDL_Delay(STATE_DELAY_MIN);
}
}
void quit(State* self)
{
imgui_free();
generate_preview_free(&self->generatePreview);
preview_free(&self->preview);
editor_free(&self->editor);
resources_free(&self->resources);
/*
Mix_CloseAudio();
Mix_Quit();
*/
SDL_GL_DestroyContext(self->glContext);
SDL_Quit();
settings_save(&self->settings);
log_info(STATE_QUIT_INFO);
log_free();
}

View File

@@ -1,56 +1,33 @@
#pragma once
#include "imgui.h"
#include <SDL3/SDL.h>
#define STATE_INIT_INFO "Initializing anm2ed (Version 1.1)"
#define STATE_SDL_INIT_ERROR "Failed to initialize SDL! {}"
#define STATE_SDL_INIT_INFO "Initialized SDL"
#define STATE_MIX_INIT_WARNING "Unable to initialize SDL_mixer! {}"
#define STATE_MIX_AUDIO_DEVICE_INIT_WARNING "Unable to initialize audio device! {}"
#define STATE_MIX_INIT_INFO "Initialized SDL_mixer"
#define STATE_GL_CONTEXT_INIT_ERROR "Failed to initialize OpenGL context! {}"
#define STATE_GLAD_INIT_ERROR "Failed to initialize GLAD!"
#define STATE_GL_CONTEXT_INIT_INFO "Initialized OpenGL context (OpenGL {})"
#define STATE_QUIT_INFO "Exiting..."
#define STATE_GL_LINE_WIDTH 2.0f
#include "dockspace.h"
#define STATE_DELAY_MIN 1
namespace anm2ed::state
{
class State
{
void tick(settings::Settings& settings);
void update(SDL_Window*& window, settings::Settings& settings);
void render(SDL_Window*& window, settings::Settings& settings);
#define STATE_MIX_FLAGS (MIX_INIT_MP3 | MIX_INIT_OGG | MIX_INIT_WAV)
#define STATE_MIX_SAMPLE_RATE 44100
#define STATE_MIX_FORMAT MIX_DEFAULT_FORMAT
#define STATE_MIX_CHANNELS 2
#define STATE_MIX_CHUNK_SIZE 1024
#define STATE_MIX_DEVICE NULL
#define STATE_MIX_ALLOWED_CHANGES SDL_AUDIO_ALLOW_FORMAT_CHANGE
public:
bool isQuit{};
dialog::Dialog dialog;
resources::Resources resources;
playback::Playback playback;
document_manager::DocumentManager manager;
#define STATE_GL_VERSION_MAJOR 3
#define STATE_GL_VERSION_MINOR 3
taskbar::Taskbar taskbar;
documents::Documents documents;
dockspace::Dockspace dockspace;
struct State {
SDL_Window* window;
SDL_GLContext glContext;
Imgui imgui;
Dialog dialog;
Editor editor;
Preview preview;
GeneratePreview generatePreview;
Anm2 anm2;
Anm2Reference reference;
Resources resources;
Settings settings;
Snapshots snapshots;
Clipboard clipboard;
std::string argument{};
std::string lastAction{};
uint64_t lastTick{};
uint64_t tick{};
uint64_t update{};
uint64_t lastUpdate{};
bool isRunning = true;
uint64_t previousTick{};
uint64_t previousUpdate{};
State(SDL_Window*& window, std::vector<std::string>& arguments);
void loop(SDL_Window*& window, settings::Settings& settings);
};
};
bool sdl_init(State* self, bool isTestMode);
void init(State* state);
void loop(State* state);
void quit(State* state);

266
src/taskbar.cpp Normal file
View File

@@ -0,0 +1,266 @@
#include "taskbar.h"
#include <imgui/imgui.h>
#include <ranges>
#include "imgui.h"
#include "types.h"
using namespace anm2ed::settings;
using namespace anm2ed::dialog;
using namespace anm2ed::document_manager;
using namespace anm2ed::imgui;
using namespace anm2ed::types;
namespace anm2ed::taskbar
{
void Taskbar::update(Settings& settings, Dialog& dialog, DocumentManager& manager, bool& isQuit)
{
auto document = manager.get();
auto animation = document ? document->animation_get() : nullptr;
if (ImGui::BeginMainMenuBar())
{
height = ImGui::GetWindowSize().y;
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.anm2_new();
if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.anm2_open();
ImGui::BeginDisabled(!document);
{
if (ImGui::MenuItem("Save", settings.shortcutSave.c_str())) manager.save();
if (ImGui::MenuItem("Save As", settings.shortcutSaveAs.c_str())) dialog.anm2_save();
if (ImGui::MenuItem("Explore XML Location")) dialog.file_explorer_open(document->directory_get());
}
ImGui::EndDisabled();
ImGui::Separator();
if (ImGui::MenuItem("Exit", settings.shortcutExit.c_str())) isQuit = true;
ImGui::EndMenu();
}
if (dialog.is_selected_file(dialog::ANM2_NEW))
{
manager.new_(dialog.path);
dialog.reset();
}
if (dialog.is_selected_file(dialog::ANM2_OPEN))
{
manager.open(dialog.path);
dialog.reset();
}
if (dialog.is_selected_file(dialog::ANM2_SAVE))
{
manager.save(dialog.path);
dialog.reset();
}
if (ImGui::BeginMenu("Wizard"))
{
ImGui::BeginDisabled(!animation);
{
ImGui::MenuItem("Generate Animation From Grid");
ImGui::MenuItem("Change All Frame Properties");
}
ImGui::EndDisabled();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Playback"))
{
ImGui::MenuItem("Always Loop", nullptr, &settings.playbackIsLoop);
ImGui::SetItemTooltip("%s", "Animations will always loop during playback, even if looping isn't set.");
ImGui::MenuItem("Clamp Playhead", nullptr, &settings.playbackIsClampPlayhead);
ImGui::SetItemTooltip("%s", "The playhead will always clamp to the animation's length.");
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Window"))
{
for (auto [i, member] : std::views::enumerate(WINDOW_MEMBERS))
ImGui::MenuItem(WINDOW_STRINGS[i], nullptr, &(settings.*member));
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Settings"))
{
if (ImGui::MenuItem("Configure")) isOpenConfigurePopup = true;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
if (ImGui::MenuItem("About")) isOpenAboutPopup = true;
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
if (isOpenAboutPopup)
{
ImGui::OpenPopup("About");
isOpenAboutPopup = false;
}
if (isOpenConfigurePopup)
{
ImGui::OpenPopup("Configure");
editSettings = settings;
isOpenConfigurePopup = false;
}
auto viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * 0.5f));
if (ImGui::BeginPopupModal("Configure", nullptr, ImGuiWindowFlags_NoResize))
{
auto childSize = imgui::size_with_footer_get(2);
if (ImGui::BeginTabBar("##Configure Tabs"))
{
if (ImGui::BeginTabItem("View"))
{
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
ImGui::InputFloat("Zoom Step", &editSettings.viewZoomStep, 10.0f, 10.0f, "%.2f");
ImGui::SetItemTooltip("%s", "When zooming in/out with mouse or shortcut, this value will be used.");
editSettings.viewZoomStep = glm::clamp(editSettings.viewZoomStep, 1.0f, 250.0f);
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Video"))
{
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
ImGui::InputFloat("Display Scale", &editSettings.displayScale, 0.25f, 0.25f, "%.2f");
ImGui::SetItemTooltip("%s", "Change the scale of the display.");
editSettings.displayScale = glm::clamp(editSettings.displayScale, 0.5f, 2.0f);
ImGui::Checkbox("Vsync", &editSettings.isVsync);
ImGui::SetItemTooltip("%s",
"Toggle vertical sync; synchronizes program update rate with monitor refresh rate.");
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Shortcuts"))
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
if (ImGui::BeginTable("Shortcuts", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("Shortcut");
ImGui::TableSetupColumn("Value");
ImGui::TableHeadersRow();
for (int i = 0; i < SHORTCUT_COUNT; ++i)
{
bool isSelected = selectedShortcut == i;
ShortcutMember member = SHORTCUT_MEMBERS[i];
std::string* settingString = &(editSettings.*member);
std::string chordString = isSelected ? "" : *settingString;
ImGui::PushID(i);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(SHORTCUT_STRINGS[i]);
ImGui::TableSetColumnIndex(1);
if (ImGui::Selectable(chordString.c_str(), isSelected)) selectedShortcut = i;
ImGui::PopID();
if (isSelected)
{
ImGuiKeyChord chord{ImGuiKey_None};
if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) chord |= ImGuiMod_Ctrl;
if (ImGui::IsKeyDown(ImGuiMod_Shift)) chord |= ImGuiMod_Shift;
if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt;
if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super;
for (auto& key : imgui::KEY_MAP | std::views::values)
{
if (ImGui::IsKeyPressed(key))
{
chord |= key;
*settingString = imgui::chord_to_string(chord);
selectedShortcut = -1;
break;
}
}
}
}
ImGui::EndTable();
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
auto widgetSize = imgui::widget_size_with_row_get(3);
if (ImGui::Button("Save", widgetSize))
{
settings = editSettings;
ImGui::CloseCurrentPopup();
}
ImGui::SetItemTooltip("Use the configured settings.");
ImGui::SameLine();
if (ImGui::Button("Use Default Settings", widgetSize)) editSettings = Settings();
ImGui::SetItemTooltip("Reset the settings to their defaults.");
ImGui::SameLine();
if (ImGui::Button("Close", widgetSize)) ImGui::CloseCurrentPopup();
ImGui::SetItemTooltip("Close without updating settings.");
ImGui::EndPopup();
}
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * 0.5f));
if (ImGui::BeginPopupModal("About", nullptr, ImGuiWindowFlags_NoResize))
{
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutNew), ImGuiInputFlags_RouteGlobal)) dialog.anm2_new();
if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutOpen), ImGuiInputFlags_RouteGlobal)) dialog.anm2_open();
if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutSave), ImGuiInputFlags_RouteGlobal)) manager.save();
if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutSaveAs), ImGuiInputFlags_RouteGlobal))
dialog.anm2_save();
if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutExit), ImGuiInputFlags_RouteGlobal)) isQuit = true;
}
}

22
src/taskbar.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include "dialog.h"
#include "document_manager.h"
#include "settings.h"
namespace anm2ed::taskbar
{
class Taskbar
{
bool isOpenConfigurePopup{};
bool isOpenAboutPopup{};
int selectedShortcut{-1};
settings::Settings editSettings{};
public:
float height{};
void update(settings::Settings& settings, dialog::Dialog& dialog, document_manager::DocumentManager& manager,
bool& isQuit);
};
};

View File

@@ -1,116 +1,128 @@
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#pragma GCC diagnostic ignored "-Wunused-function"
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
#include "texture.h"
#include <lunasvg.h>
#include <memory>
#include <utility>
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
#define STBI_ONLY_PNG
#define STBI_NO_FAILURE_STRINGS
#define STBI_NO_HDR
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
static void _texture_gl_set(Texture* self, const uint8_t* data) {
if (self->id == GL_ID_NONE)
glGenTextures(1, &self->id);
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
glBindTexture(GL_TEXTURE_2D, self->id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self->size.x, self->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
}
using namespace glm;
std::vector<uint8_t> texture_download(const Texture* self) {
std::vector<uint8_t> pixels(self->size.x * self->size.y * TEXTURE_CHANNELS);
glBindTexture(GL_TEXTURE_2D, self->id);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
return pixels;
}
bool texture_from_path_init(Texture* self, const std::string& path) {
uint8_t* data = stbi_load(path.c_str(), &self->size.x, &self->size.y, nullptr, TEXTURE_CHANNELS);
if (!data) {
log_error(std::format(TEXTURE_INIT_ERROR, path));
return false;
namespace anm2ed::texture
{
bool Texture::is_valid()
{
return id != 0;
}
self->isInvalid = false;
void Texture::download(std::vector<uint8_t>& pixels)
{
pixels.resize(size.x * size.y * CHANNELS);
glBindTexture(GL_TEXTURE_2D, id);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindTexture(GL_TEXTURE_2D, 0);
}
log_info(std::format(TEXTURE_INIT_INFO, path));
void Texture::init(const uint8_t* data, bool isDownload)
{
glGenTextures(1, &id);
_texture_gl_set(self, data);
glBindTexture(GL_TEXTURE_2D, id);
return true;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
bool texture_from_rgba_init(Texture* self, ivec2 size, const uint8_t* data) {
*self = Texture{};
self->size = size;
self->isInvalid = false;
glBindTexture(GL_TEXTURE_2D, 0);
_texture_gl_set(self, data);
if (isDownload) download(pixels);
}
return true;
}
Texture::Texture() = default;
bool texture_from_rgba_write(const std::string& path, const uint8_t* data, ivec2 size) {
bool isSuccess = stbi_write_png(path.c_str(), size.x, size.y, TEXTURE_CHANNELS, data, size.x * TEXTURE_CHANNELS);
if (!isSuccess)
log_error(std::format(TEXTURE_SAVE_ERROR, path));
else
log_info(std::format(TEXTURE_SAVE_INFO, path));
Texture::~Texture()
{
if (is_valid()) glDeleteTextures(1, &id);
}
return isSuccess;
}
Texture::Texture(Texture&& other)
{
*this = std::move(other);
}
bool texture_from_memory_init(Texture* self, ivec2 size, const uint8_t* data, size_t length) {
*self = Texture{};
self->size = size;
Texture& Texture::operator=(Texture&& other)
{
if (this != &other)
{
if (is_valid()) glDeleteTextures(1, &id);
id = std::exchange(other.id, 0);
size = std::exchange(other.size, {});
filter = other.filter;
channels = other.channels;
pixels = std::move(other.pixels);
}
return *this;
}
u8* textureData = stbi_load_from_memory(data, length, &self->size.x, &self->size.y, nullptr, TEXTURE_CHANNELS);
Texture::Texture(const char* svgData, size_t svgDataLength, ivec2 svgSize)
{
if (!svgData) return;
if (!textureData)
return false;
const std::unique_ptr<lunasvg::Document> document = lunasvg::Document::loadFromData(svgData, svgDataLength);
if (!document) return;
self->isInvalid = false;
const lunasvg::Bitmap bitmap = document->renderToBitmap(svgSize.x, svgSize.y, 0);
if (bitmap.width() == 0 || bitmap.height() == 0) return;
_texture_gl_set(self, textureData);
size = svgSize;
filter = GL_LINEAR;
init(bitmap.data());
}
return true;
}
Texture::Texture(const std::string& pngPath, bool isDownload)
{
if (const uint8* data = stbi_load(pngPath.c_str(), &size.x, &size.y, nullptr, CHANNELS); data)
{
init(data, isDownload);
stbi_image_free((void*)data);
}
}
bool texture_from_gl_write(Texture* self, const std::string& path) { return texture_from_rgba_write(path, texture_download(self).data(), self->size); }
bool Texture::write_png(const std::string& path)
{
std::vector<uint8_t> pixels;
download(pixels);
const bool isSuccess = stbi_write_png(path.c_str(), size.x, size.y, CHANNELS, pixels.data(), size.x * CHANNELS);
return isSuccess;
}
void texture_free(Texture* self) {
if (self->isInvalid)
return;
void Texture::bind(GLuint unit)
{
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, id);
}
glDeleteTextures(1, &self->id);
*self = Texture{};
}
bool texture_pixel_set(Texture* self, ivec2 position, vec4 color) {
if (position.x < 0 || position.y < 0 || position.x >= self->size.x || position.y >= self->size.y)
return false;
uint8_t rgba8[4] = {FLOAT_TO_UINT8(color.r), FLOAT_TO_UINT8(color.g), FLOAT_TO_UINT8(color.b), FLOAT_TO_UINT8(color.a)};
glBindTexture(GL_TEXTURE_2D, self->id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, position.x, position.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba8);
return true;
}
void Texture::unbind(GLuint unit)
{
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, 0);
}
}

View File

@@ -1,26 +1,36 @@
#pragma once
#include "log.h"
#include <string>
#include <vector>
#define TEXTURE_CHANNELS 4
#define TEXTURE_INIT_INFO "Initialized texture from file: {}"
#define TEXTURE_INIT_ERROR "Failed to initialize texture from file: {}"
#define TEXTURE_SAVE_INFO "Saved texture to: {}"
#define TEXTURE_SAVE_ERROR "Failed to save texture to: {}"
#include <glad/glad.h>
#include <glm/glm.hpp>
struct Texture {
GLuint id = GL_ID_NONE;
ivec2 size{};
bool isInvalid = true;
namespace anm2ed::texture
{
constexpr auto CHANNELS = 4;
auto operator<=>(const Texture&) const = default;
};
class Texture
{
public:
GLuint id{};
glm::ivec2 size{};
GLint filter = GL_NEAREST;
int channels{};
std::vector<uint8_t> pixels{};
bool texture_from_gl_write(Texture* self, const std::string& path);
bool texture_from_path_init(Texture* self, const std::string& path);
bool texture_from_rgba_init(Texture* self, ivec2 size, const uint8_t* data);
bool texture_from_rgba_write(const std::string& path, const uint8_t* data, ivec2 size);
bool texture_pixel_set(Texture* self, ivec2 position, vec4 color);
void texture_free(Texture* self);
bool texture_from_memory_init(Texture* self, ivec2 size, const uint8_t* data, size_t length);
std::vector<uint8_t> texture_download(const Texture* self);
bool is_valid();
void download(std::vector<uint8_t>& pixels);
void init(const uint8_t* data, bool isDownload = false);
Texture();
~Texture();
Texture(Texture&& other);
Texture& operator=(Texture&& other);
Texture(const char* svgData, size_t svgDataLength, glm::ivec2 svgSize);
Texture(const std::string& pngPath, bool isDownload = false);
bool write_png(const std::string& path);
void bind(GLuint unit = 0);
void unbind(GLuint unit = 0);
};
}

701
src/timeline.cpp Normal file
View File

@@ -0,0 +1,701 @@
#include "timeline.h"
#include <ranges>
#include <imgui_internal.h>
#include "imgui.h"
using namespace anm2ed::types;
using namespace anm2ed::document_manager;
using namespace anm2ed::resources;
using namespace anm2ed::settings;
using namespace anm2ed::playback;
using namespace glm;
namespace anm2ed::timeline
{
constexpr auto ROOT_COLOR = ImVec4(0.140f, 0.310f, 0.560f, 1.000f);
constexpr auto ROOT_COLOR_ACTIVE = ImVec4(0.240f, 0.520f, 0.880f, 1.000f);
constexpr auto ROOT_COLOR_HOVERED = ImVec4(0.320f, 0.640f, 1.000f, 1.000f);
constexpr auto LAYER_COLOR = ImVec4(0.640f, 0.320f, 0.110f, 1.000f);
constexpr auto LAYER_COLOR_ACTIVE = ImVec4(0.840f, 0.450f, 0.170f, 1.000f);
constexpr auto LAYER_COLOR_HOVERED = ImVec4(0.960f, 0.560f, 0.240f, 1.000f);
constexpr auto NULL_COLOR = ImVec4(0.140f, 0.430f, 0.200f, 1.000f);
constexpr auto NULL_COLOR_ACTIVE = ImVec4(0.250f, 0.650f, 0.350f, 1.000f);
constexpr auto NULL_COLOR_HOVERED = ImVec4(0.350f, 0.800f, 0.480f, 1.000f);
constexpr auto TRIGGER_COLOR = ImVec4(0.620f, 0.150f, 0.260f, 1.000f);
constexpr auto TRIGGER_COLOR_ACTIVE = ImVec4(0.820f, 0.250f, 0.380f, 1.000f);
constexpr auto TRIGGER_COLOR_HOVERED = ImVec4(0.950f, 0.330f, 0.490f, 1.000f);
constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f);
constexpr auto FRAME_TIMELINE_COLOR = ImVec4(0.106f, 0.184f, 0.278f, 1.000f);
constexpr auto FRAME_BORDER_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.05f);
constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f;
constexpr auto TEXT_MULTIPLE_COLOR = to_imvec4(color::WHITE);
constexpr auto PLAYHEAD_LINE_COLOR = to_imvec4(color::WHITE);
constexpr auto FRAME_MULTIPLE = 5;
constexpr auto HELP_FORMAT = R"(- Press {} to decrement time.
- Press {} to increment time.
- Press {} to extend the selected frame, by one frame.
- Press {} to shorten the selected frame, by one frame.
- Hold Alt while clicking a non-trigger frame to toggle interpolation.)";
void Timeline::item_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation,
Settings& settings, Resources& resources, anm2::Type type, int id, int& index)
{
auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false;
auto isActive = reference.itemType == type && reference.itemID == id;
std::string label = "##None";
icon::Type icon{};
ImVec4 color{};
switch (type)
{
case anm2::ROOT:
label = "Root";
icon = icon::ROOT;
color = isActive ? ROOT_COLOR_ACTIVE : ROOT_COLOR;
break;
case anm2::LAYER:
label = std::format("#{} {}", id, anm2.content.layers[id].name);
icon = icon::LAYER;
color = isActive ? LAYER_COLOR_ACTIVE : LAYER_COLOR;
break;
case anm2::NULL_:
label = std::format("#{} {}", id, anm2.content.nulls[id].name);
icon = icon::NULL_;
color = isActive ? NULL_COLOR_ACTIVE : NULL_COLOR;
break;
case anm2::TRIGGER:
label = "Triggers";
icon = icon::TRIGGERS;
color = isActive ? TRIGGER_COLOR_ACTIVE : TRIGGER_COLOR;
break;
default:
break;
}
color = !isVisible ? to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER) : color;
ImGui::PushStyleColor(ImGuiCol_ChildBg, color);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x,
ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2));
if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders))
{
if (type != anm2::NONE)
{
anm2::Reference itemReference = {reference.animationIndex, type, id};
if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference;
ImGui::Image(resources.icons[icon].id, imgui::icon_size_get());
ImGui::SameLine();
ImGui::TextUnformatted(label.c_str());
anm2::Item* item = animation->item_get(type, id);
bool& isVisible = item->isVisible;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2());
ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE;
if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, imgui::icon_size_get()))
isVisible = !isVisible;
ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show.");
if (type == anm2::NULL_)
{
auto& null = anm2.content.nulls.at(id);
auto& isShowRect = null.isShowRect;
auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT;
ImGui::SetCursorPos(
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, imgui::icon_size_get()))
isShowRect = !isShowRect;
ImGui::SetItemTooltip(isShowRect ? "The null's rect is shown. Press to hide."
: "The null's rect is hidden. Press to show.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
}
else
{
auto cursorPos = ImGui::GetCursorPos();
auto& isShowUnused = settings.timelineIsShowUnused;
ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2());
auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED;
if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, imgui::icon_size_get()))
isShowUnused = !isShowUnused;
ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide."
: "Unused layers/nulls are hidden. Press to show.");
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorPos(cursorPos);
ImGui::Text("(?)");
ImGui::SetItemTooltip("%s", std::format(HELP_FORMAT, settings.shortcutNextFrame, settings.shortcutPreviousFrame,
settings.shortcutShortenFrame, settings.shortcutExtendFrame)
.c_str());
}
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
index++;
}
void Timeline::items_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation,
Settings& settings, Resources& resources)
{
auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders))
{
auto itemsListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y -
ImGui::GetTextLineHeightWithSpacing() -
ImGui::GetStyle().ItemSpacing.y * 2);
if (ImGui::BeginChild("##Items List Child", itemsListChildSize, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar))
{
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2());
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 0.0f);
if (ImGui::BeginTable("##Item Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY))
{
ImGui::GetCurrentWindow()->Flags |= ImGuiWindowFlags_NoScrollWithMouse;
ImGui::SetScrollY(scroll.y);
int index{};
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("##Items");
auto item_child_row = [&](anm2::Type type, int id = -1)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
item_child(anm2, reference, animation, settings, resources, type, id, index);
};
item_child_row(anm2::NONE);
if (animation)
{
item_child_row(anm2::ROOT);
for (auto& id : animation->layerOrder)
{
if (anm2::Item* item = animation->item_get(anm2::LAYER, id); item)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
item_child_row(anm2::LAYER, id);
}
for (auto& id : animation->nullAnimations | std::views::keys)
{
if (anm2::Item* item = animation->item_get(anm2::NULL_, id); item)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
item_child_row(anm2::NULL_, id);
}
item_child_row(anm2::TRIGGER);
}
if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Dummy(ImVec2(0, style.ScrollbarSize));
}
ImGui::EndTable();
}
ImGui::PopStyleVar(2);
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
ImGui::BeginDisabled(!animation);
{
ImGui::Button("Add", widgetSize);
ImGui::SameLine();
ImGui::Button("Remove", widgetSize);
}
ImGui::EndDisabled();
}
ImGui::EndChild();
}
void Timeline::frame_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources,
Playback& playback, anm2::Type type, int id, int& index, float width)
{
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false;
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
auto childSize = ImVec2(width, ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2));
ImVec4 color{};
ImVec4 colorActive{};
ImVec4 colorHovered{};
ImVec4 colorHidden{};
ImVec4 colorActiveHidden{};
ImVec4 colorHoveredHidden{};
ImGui::PopStyleVar(2);
ImGui::PushID(index);
switch (type)
{
case anm2::ROOT:
color = ROOT_COLOR;
colorActive = ROOT_COLOR_ACTIVE;
colorHovered = ROOT_COLOR_HOVERED;
break;
case anm2::LAYER:
color = LAYER_COLOR;
colorActive = LAYER_COLOR_ACTIVE;
colorHovered = LAYER_COLOR_HOVERED;
break;
case anm2::NULL_:
color = NULL_COLOR;
colorActive = NULL_COLOR_ACTIVE;
colorHovered = NULL_COLOR_HOVERED;
break;
case anm2::TRIGGER:
color = TRIGGER_COLOR;
colorActive = TRIGGER_COLOR_ACTIVE;
colorHovered = TRIGGER_COLOR_HOVERED;
break;
default:
color = ImGui::GetStyleColorVec4(ImGuiCol_Button);
colorActive = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive);
colorHovered = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered);
break;
}
colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER);
colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER);
colorHoveredHidden = to_imvec4(to_vec4(colorHovered) * COLOR_HIDDEN_MULTIPLIER);
ImGui::PushStyleColor(ImGuiCol_Button, isVisible ? color : colorHidden);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, isVisible ? colorActive : colorActiveHidden);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isVisible ? colorHovered : colorHoveredHidden);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2());
if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders))
{
auto length = animation ? animation->frameNum : anm2.animations.length();
auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y);
auto framesSize = ImVec2(frameSize.x * length, frameSize.y);
auto cursorPos = ImGui::GetCursorPos();
auto cursorScreenPos = ImGui::GetCursorScreenPos();
auto imageSize = vec2(ImGui::GetTextLineHeight());
auto border = ImGui::GetStyle().FrameBorderSize;
auto borderLineLength = frameSize.y / 5;
auto scrollX = ImGui::GetScrollX();
auto available = ImGui::GetContentRegionAvail();
auto frameMin = std::max(0, (int)std::floor(scrollX / frameSize.x) - 1);
auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil(scrollX + available.x / frameSize.x) + 1);
auto drawList = ImGui::GetWindowDrawList();
pickerLineDrawList = drawList;
if (type == anm2::NONE)
{
drawList->AddRectFilled(cursorScreenPos,
ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y),
ImGui::GetColorU32(FRAME_TIMELINE_COLOR));
for (int i = frameMin; i < frameMax; i++)
{
auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y);
drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + border, frameScreenPos.y + borderLineLength),
ImGui::GetColorU32(FRAME_BORDER_COLOR));
drawList->AddRect(ImVec2(frameScreenPos.x, frameScreenPos.y + frameSize.y - borderLineLength),
ImVec2(frameScreenPos.x + border, frameScreenPos.y + frameSize.y),
ImGui::GetColorU32(FRAME_BORDER_COLOR));
if (i % FRAME_MULTIPLE == 0)
{
auto string = std::to_string(i);
auto textSize = ImGui::CalcTextSize(string.c_str());
auto textPos = ImVec2(frameScreenPos.x + (frameSize.x - textSize.x) / 2,
frameScreenPos.y + (frameSize.y - textSize.y) / 2);
drawList->AddRectFilled(frameScreenPos,
ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y),
ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR));
drawList->AddText(textPos, ImGui::GetColorU32(TEXT_MULTIPLE_COLOR), string.c_str());
}
}
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0))
isDragging = true;
if (isDragging)
{
auto childPos = ImGui::GetWindowPos();
auto mousePos = ImGui::GetIO().MousePos;
auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y);
playback.time = floorf(localMousePos.x / frameSize.x);
reference.frameTime = playback.time;
}
playback.clamp(settings.playbackIsClampPlayhead ? length : anm2::FRAME_NUM_MAX);
if (ImGui::IsMouseReleased(0)) isDragging = false;
ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * floorf(playback.time), cursorPos.y));
ImGui::Image(resources.icons[icon::PLAYHEAD].id, frameSize);
}
else if (animation)
{
anm2::Reference itemReference = {reference.animationIndex, type, id};
if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(0)) reference = itemReference;
for (int i = frameMin; i < frameMax; i++)
{
auto frameScreenPos = ImVec2(cursorScreenPos.x + (frameSize.x * i), cursorScreenPos.y);
if (i % FRAME_MULTIPLE == 0)
{
drawList->AddRectFilled(frameScreenPos,
ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y),
ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR));
}
drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y),
ImGui::GetColorU32(FRAME_BORDER_COLOR));
}
auto item = animation->item_get(type, id);
auto frameTime = 0;
anm2::Reference baseReference = {reference.animationIndex, reference.itemType, reference.itemID,
reference.frameIndex};
for (auto [i, frame] : std::views::enumerate(item->frames))
{
anm2::Reference frameReference = {reference.animationIndex, type, id, (int)i};
auto isSelected = baseReference == frameReference;
frameTime += frame.delay;
if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
ImGui::PushID(i);
auto size = ImVec2(frameSize.x * frame.delay, frameSize.y);
auto icon = type == anm2::TRIGGER ? icon::TRIGGER
: frame.isInterpolated ? icon::INTERPOLATED
: icon::UNINTERPOLATED;
if (type == anm2::TRIGGER)
ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * frame.atFrame, cursorPos.y));
if (!frame.isVisible)
{
ImGui::PushStyleColor(ImGuiCol_Button, colorHidden);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, colorActiveHidden);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colorHoveredHidden);
}
if (ImGui::Button("##Frame Button", size))
{
if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt)) frame.isInterpolated = !frame.isInterpolated;
if (type == anm2::LAYER) document.referenceSpritesheet = anm2.content.layers[id].spritesheetID;
reference = frameReference;
reference.frameTime = frameTime;
}
if (type != anm2::TRIGGER) ImGui::SameLine();
if (!frame.isVisible) ImGui::PopStyleColor(3);
auto imageMin = ImVec2(ImGui::GetItemRectMin().x,
ImGui::GetItemRectMax().y - (ImGui::GetItemRectSize().y / 2) - (imageSize.y / 2));
auto imageMax = to_imvec2(to_vec2(imageMin) + imageSize);
drawList->AddImage(resources.icons[icon].id, imageMin, imageMax);
if (isSelected) ImGui::PopStyleColor();
ImGui::PopID();
}
}
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
index++;
ImGui::PopID();
}
void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources,
Playback& playback)
{
auto& anm2 = document.anm2;
auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15;
auto cursorPos = ImGui::GetCursorPos();
ImGui::SetCursorPos(ImVec2(cursorPos.x + itemsChildWidth, cursorPos.y));
auto framesChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Frames Child", framesChildSize, ImGuiChildFlags_Borders))
{
auto viewListChildSize =
ImVec2(ImGui::GetContentRegionAvail().x,
ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - style.ItemSpacing.y * 2);
auto childWidth = ImGui::GetContentRegionAvail().x > anm2.animations.length() * ImGui::GetTextLineHeight()
? ImGui::GetContentRegionAvail().x
: anm2.animations.length() * ImGui::GetTextLineHeight();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
if (ImGui::BeginChild("##Frames List Child", viewListChildSize, true, ImGuiWindowFlags_HorizontalScrollbar))
{
auto cursorScreenPos = ImGui::GetCursorScreenPos();
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2());
if (ImGui::BeginTable("##Frames List Table", 1,
ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY))
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
window->Flags |= ImGuiWindowFlags_NoScrollWithMouse;
scroll.x = ImGui::GetScrollX();
scroll.y = ImGui::GetScrollY();
isHorizontalScroll = window->ScrollbarX;
if (isWindowHovered)
{
auto& io = ImGui::GetIO();
auto lineHeight = ImGui::GetTextLineHeightWithSpacing() * 2;
scroll.x -= io.MouseWheelH * lineHeight;
scroll.y -= io.MouseWheel * lineHeight;
}
ImGui::SetScrollX(scroll.x);
ImGui::SetScrollY(scroll.y);
int index{};
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("##Frames");
auto frames_child_row = [&](anm2::Type type, int id = -1)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
frame_child(document, animation, settings, resources, playback, type, id, index, childWidth);
};
frames_child_row(anm2::NONE);
if (animation)
{
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
frames_child_row(anm2::ROOT);
for (auto& id : animation->layerOrder)
{
if (auto item = animation->item_get(anm2::LAYER, id); item)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
frames_child_row(anm2::LAYER, id);
}
for (auto& id : animation->nullAnimations | std::views::keys)
{
if (auto item = animation->item_get(anm2::NULL_, id); item)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
frames_child_row(anm2::NULL_, id);
}
frames_child_row(anm2::TRIGGER);
ImGui::PopStyleVar();
}
ImGui::EndTable();
}
ImDrawList* windowDrawList = ImGui::GetWindowDrawList();
auto frameSize = ImVec2(ImGui::GetTextLineHeight(),
ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2));
auto linePos = ImVec2(cursorScreenPos.x + frameSize.x * floorf(playback.time) + (frameSize.x / 2) - scroll.x,
cursorScreenPos.y + frameSize.y);
auto lineSize =
ImVec2((PLAYHEAD_LINE_THICKNESS / 2.0f),
viewListChildSize.y - frameSize.y - (isHorizontalScroll ? ImGui::GetStyle().ScrollbarSize : 0.0f));
auto rectMin = windowDrawList->GetClipRectMin();
auto rectMax = windowDrawList->GetClipRectMax();
pickerLineDrawList->PushClipRect(rectMin, rectMax);
pickerLineDrawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y),
ImGui::GetColorU32(PLAYHEAD_LINE_COLOR));
pickerLineDrawList->PopClipRect();
ImGui::PopStyleVar();
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
ImGui::SetCursorPos(
ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y));
auto widgetSize = imgui::widget_size_with_row_get(9);
ImGui::BeginDisabled(!animation);
{
auto label = playback.isPlaying ? "Pause" : "Play";
auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation.";
imgui::shortcut(settings.shortcutPlayPause, true);
if (ImGui::Button(label, widgetSize)) playback.toggle();
imgui::set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause);
ImGui::SameLine();
imgui::shortcut(settings.shortcutAdd, true);
ImGui::Button("Insert Frame", widgetSize);
imgui::set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd);
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove, true);
ImGui::Button("Delete Frame", widgetSize);
imgui::set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove);
ImGui::SameLine();
ImGui::Button("Bake", widgetSize);
ImGui::SetItemTooltip("%s", "Turn interpolated frames into uninterpolated ones.");
ImGui::SameLine();
if (ImGui::Button("Fit Animation Length", widgetSize)) animation->frameNum = animation->length();
ImGui::SetItemTooltip("%s", "The animation length will be set to the effective length of the animation.");
ImGui::SameLine();
ImGui::SetNextItemWidth(widgetSize.x);
ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value<int>(), step::NORMAL,
step::FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0);
if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX);
ImGui::SetItemTooltip("%s", "Set the animation's length.");
ImGui::SameLine();
ImGui::SetNextItemWidth(widgetSize.x);
ImGui::Checkbox("Loop", animation ? &animation->isLoop : &dummy_value<bool>());
ImGui::SetItemTooltip("%s", "Toggle the animation looping.");
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::SetNextItemWidth(widgetSize.x);
ImGui::InputInt("FPS", &anm2.info.fps, 1, 5);
anm2.info.fps = clamp(anm2.info.fps, anm2::FPS_MIN, anm2::FPS_MAX);
ImGui::SetItemTooltip("%s", "Set the FPS of all animations.");
ImGui::SameLine();
ImGui::SetNextItemWidth(widgetSize.x);
imgui::input_text_string("Author", &anm2.info.createdBy);
ImGui::SetItemTooltip("%s", "Set the author of the document.");
ImGui::PopStyleVar();
}
ImGui::EndChild();
ImGui::SetCursorPos(cursorPos);
}
void Timeline::update(DocumentManager& manager, Settings& settings, Resources& resources, Playback& playback)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto animation = document.animation_get();
style = ImGui::GetStyle();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::Begin("Timeline", &settings.windowIsTimeline))
{
isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
frames_child(document, animation, settings, resources, playback);
items_child(anm2, reference, animation, settings, resources);
}
ImGui::PopStyleVar();
ImGui::End();
if (imgui::shortcut(settings.shortcutPlayPause, false, true)) playback.toggle();
if (animation)
{
if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutPreviousFrame)))
playback.decrement(animation->frameNum);
if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutNextFrame)))
playback.increment(animation->frameNum);
}
if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutShortenFrame)))
if (auto frame = anm2.frame_get(reference); frame) frame->shorten();
if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutExtendFrame)))
if (auto frame = anm2.frame_get(reference); frame) frame->extend();
}
}

35
src/timeline.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "anm2.h"
#include "document.h"
#include "document_manager.h"
#include "playback.h"
#include "resources.h"
#include "settings.h"
namespace anm2ed::timeline
{
class Timeline
{
bool isDragging{};
bool isWindowHovered{};
bool isHorizontalScroll{};
glm::vec2 scroll{};
ImDrawList* pickerLineDrawList{};
ImGuiStyle style{};
void item_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation,
settings::Settings& settings, resources::Resources& resources, anm2::Type type, int id, int& index);
void items_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation,
settings::Settings& settings, resources::Resources& resources);
void frame_child(document::Document& document, anm2::Animation* animation, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback, anm2::Type type, int id, int& index,
float width);
void frames_child(document::Document& document, anm2::Animation* animation, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback);
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback);
};
}

80
src/toast.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include "toast.h"
#include "log.h"
#include <imgui/imgui.h>
#include "types.h"
using namespace anm2ed::log;
using namespace anm2ed::types;
namespace anm2ed::toast
{
constexpr auto LIFETIME = 3.0f;
Toast::Toast(const std::string& message)
{
this->message = message;
lifetime = LIFETIME;
}
void Toasts::update()
{
ImGuiIO& io = ImGui::GetIO();
auto borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border);
auto textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
auto position = to_vec2(io.DisplaySize) - to_vec2(ImGui::GetStyle().ItemSpacing);
for (int i = (int)toasts.size() - 1; i >= 0; --i)
{
Toast& toast = toasts[i];
toast.lifetime -= ImGui::GetIO().DeltaTime;
if (toast.lifetime <= 0.0f)
{
toasts.erase(toasts.begin() + i);
i--;
continue;
}
auto alpha = toast.lifetime / LIFETIME;
borderColor.w = alpha;
textColor.w = alpha;
ImGui::SetNextWindowPos(to_imvec2(position), ImGuiCond_None, {1.0f, 1.0f});
ImGui::SetNextWindowSize(ImVec2(0, ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y));
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
ImGui::SetNextWindowBgAlpha(alpha);
if (ImGui::Begin(std::format("##Toast #{}", i).c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav))
{
ImGui::TextUnformatted(toast.message.c_str());
position.y -= ImGui::GetWindowSize().y + ImGui::GetStyle().ItemSpacing.y;
}
ImGui::End();
ImGui::PopStyleColor(2);
}
}
void Toasts::add(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.info(message);
}
void Toasts::add_error(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.error(message);
}
Toasts toasts;
}

29
src/toast.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <vector>
namespace anm2ed::toast
{
class Toast
{
public:
std::string message{};
float lifetime{};
Toast(const std::string& message);
};
class Toasts
{
public:
std::vector<Toast> toasts{};
void update();
void add(const std::string& message);
void add_error(const std::string& message);
};
extern Toasts toasts;
}

View File

@@ -1,37 +1,73 @@
#pragma once
#include "COMMON.h"
#include "icon.h"
#include "settings.h"
#define TOOL_STEP 1
#define TOOL_STEP_MOD 10
enum ToolType
namespace anm2ed::tool
{
TOOL_PAN,
TOOL_MOVE,
TOOL_ROTATE,
TOOL_SCALE,
TOOL_CROP,
TOOL_DRAW,
TOOL_ERASE,
TOOL_COLOR_PICKER,
TOOL_UNDO,
TOOL_REDO,
TOOL_COLOR,
TOOL_COUNT,
};
enum Type
{
PAN,
MOVE,
ROTATE,
SCALE,
CROP,
DRAW,
ERASE,
COLOR_PICKER,
UNDO,
REDO,
COLOR,
COUNT
};
const SDL_SystemCursor CURSOR_DEFAULT = SDL_SYSTEM_CURSOR_DEFAULT;
const SDL_SystemCursor TOOL_CURSORS[TOOL_COUNT] =
{
SDL_SYSTEM_CURSOR_POINTER,
SDL_SYSTEM_CURSOR_MOVE,
SDL_SYSTEM_CURSOR_CROSSHAIR,
SDL_SYSTEM_CURSOR_NE_RESIZE,
SDL_SYSTEM_CURSOR_CROSSHAIR,
SDL_SYSTEM_CURSOR_CROSSHAIR,
SDL_SYSTEM_CURSOR_CROSSHAIR,
SDL_SYSTEM_CURSOR_CROSSHAIR,
SDL_SYSTEM_CURSOR_DEFAULT,
SDL_SYSTEM_CURSOR_DEFAULT
};
struct Info
{
ImGuiMouseCursor cursor{ImGuiMouseCursor_None};
icon::Type icon{};
settings::ShortcutType shortcut{};
const char* label{};
const char* tooltip{};
};
constexpr Info INFO[] = {
{ImGuiMouseCursor_Hand, icon::PAN, settings::SHORTCUT_PAN, "##Pan",
"Use the pan tool.\nWill shift the view as the cursor is dragged.\nYou can also use the middle mouse button to "
"pan at any time."},
{ImGuiMouseCursor_ResizeAll, icon::MOVE, settings::SHORTCUT_MOVE, "##Move",
"Use the move tool.\nAnimation Preview: Will move the position of the frame."
"\nSpritesheet Editor: Will move the pivot, and holding right click will use the Crop functionality instead."
"\nUse mouse or directional keys to change the value."},
{ImGuiMouseCursor_Arrow, icon::ROTATE, settings::SHORTCUT_ROTATE, "##Rotate",
"Use the rotate tool.\nWill rotate the selected item as the cursor is dragged, or directional keys are "
"pressed.\n(Animation Preview only.)"},
{ImGuiMouseCursor_ResizeNWSE, icon::SCALE, settings::SHORTCUT_SCALE, "##Scale",
"Use the scale tool.\nWill scale the selected item as the cursor is dragged, or directional keys are "
"pressed.\n(Animation Preview only.)"},
{ImGuiMouseCursor_ResizeAll, icon::CROP, settings::SHORTCUT_CROP, "##Crop",
"Use the crop tool.\nWill produce a crop rectangle based on how the cursor is dragged."
"\nAlternatively, you can use the arrow keys and Ctrl/Shift to move the size/position, respectively."
"\nHolding right click will use the Move tool's functionality."
"\n(Spritesheet Editor only.)"},
{ImGuiMouseCursor_Hand, icon::DRAW, settings::SHORTCUT_DRAW, "##Draw",
"Draws pixels onto the selected spritesheet, with the current color.\n(Spritesheet Editor only.)"},
{ImGuiMouseCursor_Arrow, icon::ERASE, settings::SHORTCUT_ERASE, "##Erase",
"Erases pixels from the selected spritesheet.\n(Spritesheet Editor only.)"},
{ImGuiMouseCursor_Arrow, icon::COLOR_PICKER, settings::SHORTCUT_COLOR_PICKER, "##Color Picker",
"Selects a color from the canvas.\n(Spritesheet Editor only.)"},
{ImGuiMouseCursor_None, icon::UNDO, settings::SHORTCUT_UNDO, "##Undo", "Undoes the last action."},
{ImGuiMouseCursor_None, icon::REDO, settings::SHORTCUT_REDO, "##Redo", "Redoes the last action."},
{ImGuiMouseCursor_None, icon::NONE, settings::SHORTCUT_COLOR, "##Color",
"Selects the color to be used for drawing.\n(Spritesheet Editor only.)"},
};
}

106
src/tools.cpp Normal file
View File

@@ -0,0 +1,106 @@
#include "tools.h"
#include <glm/gtc/type_ptr.hpp>
#include "imgui.h"
#include "tool.h"
#include "types.h"
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
using namespace glm;
namespace anm2ed::tools
{
constexpr auto COLOR_EDIT_LABEL = "##Color Edit";
void Tools::update(Settings& settings, Resources& resources)
{
if (ImGui::Begin("Tools", &settings.windowIsTools))
{
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 2));
auto availableWidth = ImGui::GetContentRegionAvail().x;
auto size = vec2(ImGui::GetTextLineHeightWithSpacing() * 1.5f);
auto usedWidth = ImGui::GetStyle().WindowPadding.x;
auto tool_switch = [&](tool::Type type)
{
switch (type)
{
case tool::UNDO:
break;
case tool::REDO:
break;
case tool::COLOR:
if (ImGui::IsPopupOpen(COLOR_EDIT_LABEL))
ImGui::CloseCurrentPopup();
else
isOpenColorEdit = true;
break;
default:
settings.tool = type;
break;
}
};
for (int i = 0; i < tool::COUNT; i++)
{
auto& info = tool::INFO[i];
auto isSelected = settings.tool == i;
if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
auto member = SHORTCUT_MEMBERS[info.shortcut];
if (imgui::shortcut(settings.*member, false, true, i == tool::COLOR ? false : true)) tool_switch((tool::Type)i);
if (i == tool::COLOR)
{
size += to_vec2(ImGui::GetStyle().FramePadding) * 2.0f;
if (ImGui::ColorButton(info.label, to_imvec4(settings.toolColor), ImGuiColorEditFlags_NoTooltip,
to_imvec2(size)))
tool_switch((tool::Type)i);
colorEditPosition = ImGui::GetCursorScreenPos();
}
else
{
if (ImGui::ImageButton(info.label, resources.icons[info.icon].id, to_imvec2(size)))
tool_switch((tool::Type)i);
}
auto widthIncrement = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x;
usedWidth += widthIncrement;
if (usedWidth + widthIncrement < availableWidth)
ImGui::SameLine();
else
usedWidth = ImGui::GetStyle().WindowPadding.x;
imgui::set_item_tooltip_shortcut(info.tooltip, settings.*SHORTCUT_MEMBERS[info.shortcut]);
if (isSelected) ImGui::PopStyleColor();
}
ImGui::PopStyleVar();
if (isOpenColorEdit)
{
ImGui::OpenPopup(COLOR_EDIT_LABEL);
isOpenColorEdit = false;
}
ImGui::SetNextWindowPos(colorEditPosition, ImGuiCond_None);
if (ImGui::BeginPopup(COLOR_EDIT_LABEL))
{
ImGui::ColorPicker4(COLOR_EDIT_LABEL, value_ptr(settings.toolColor));
ImGui::EndPopup();
}
}
ImGui::End();
}
}

16
src/tools.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "resources.h"
#include "settings.h"
namespace anm2ed::tools
{
class Tools
{
bool isOpenColorEdit{};
ImVec2 colorEditPosition{};
public:
void update(settings::Settings& settings, resources::Resources& resources);
};
}

67
src/types.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <imgui/imgui.h>
namespace anm2ed::types::merge
{
enum Type
{
PREPEND,
APPEND,
REPLACE,
IGNORE
};
}
namespace anm2ed::types::color
{
using namespace glm;
constexpr auto WHITE = vec4(1.0);
constexpr auto BLACK = vec4(0.0, 0.0, 0.0, 1.0);
constexpr auto RED = vec4(1.0, 0.0, 0.0, 1.0);
constexpr auto GREEN = vec4(0.0, 1.0, 0.0, 1.0);
constexpr auto BLUE = vec4(0.0, 0.0, 1.0, 1.0);
constexpr auto TRANSPARENT = vec4();
}
namespace anm2ed::types::step
{
constexpr auto NORMAL = 1;
constexpr auto FAST = 10;
}
namespace anm2ed::types
{
constexpr ImVec2 to_imvec2(const glm::vec2& v) noexcept
{
return {v.x, v.y};
}
constexpr glm::vec2 to_vec2(const ImVec2& v) noexcept
{
return {v.x, v.y};
}
constexpr glm::ivec2 to_ivec2(const ImVec2& v) noexcept
{
return {v.x, v.y};
}
constexpr ImVec4 to_imvec4(const glm::vec4& v) noexcept
{
return {v.x, v.y, v.z, v.w};
}
constexpr glm::vec4 to_vec4(const ImVec4& v) noexcept
{
return {v.x, v.y, v.z, v.w};
}
template <typename T> constexpr T& dummy_value()
{
static T value{};
return value;
}
}

40
src/util.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include "util.h"
#include <algorithm>
#include <chrono>
namespace anm2ed::util::time
{
std::string get(const char* format)
{
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
auto localTime = *std::localtime(&time);
std::ostringstream timeString;
timeString << std::put_time(&localTime, format);
return timeString.str();
}
}
namespace anm2ed::util::string
{
std::string to_lower(const std::string& string)
{
std::string transformed = string;
std::ranges::transform(transformed, transformed.begin(), [](const unsigned char c) { return std::tolower(c); });
return transformed;
}
std::string replace_backslash(const std::string& string)
{
std::string transformed = string;
for (char& character : transformed)
if (character == '\\') character = '/';
return transformed;
}
bool to_bool(const std::string& string)
{
return to_lower(string) == "true";
}
}

55
src/util.h Normal file
View File

@@ -0,0 +1,55 @@
#pragma once
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
namespace anm2ed::util::time
{
std::string get(const char* format);
}
namespace anm2ed::util::string
{
std::string to_lower(const std::string& string);
std::string replace_backslash(const std::string& string);
bool to_bool(const std::string& string);
}
namespace anm2ed::util::map
{
template <typename T> int next_id_get(std::map<int, T>& map)
{
int id = 0;
for (auto& [key, value] : map)
{
if (key != id) break;
++id;
}
return id;
}
template <typename T0, typename T1> T1* find(std::map<T0, T1>& map, T0 index)
{
return map.contains(index) ? &map[index] : nullptr;
}
}
namespace anm2ed::util::unordered_map
{
template <typename T0, typename T1> T1* find(std::unordered_map<T0, T1>& map, T0 index)
{
return map.contains(index) ? &map[index] : nullptr;
}
}
namespace anm2ed::util::vector
{
template <typename T> T* find(std::vector<T>& v, int index)
{
return index >= 0 && index < (int)v.size() ? &v[index] : nullptr;
}
}

Some files were not shown because too many files have changed in this diff Show More