timeline refactor, bit broken rn

This commit is contained in:
2025-11-09 10:35:21 -05:00
parent 1e35910b0a
commit e2799b1e58
41 changed files with 2034 additions and 1374 deletions

BIN
.Icon.ico-autosave.kra Normal file

Binary file not shown.

View File

@@ -1,7 +1,7 @@
ColumnLimit: 120 ColumnLimit: 120
PointerAlignment: Left PointerAlignment: Left
ReferenceAlignment: Left ReferenceAlignment: Left
AllowShortFunctionsOnASingleLine: None AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: true AllowShortIfStatementsOnASingleLine: true
CommentPragmas: '^' CommentPragmas: '^'
BreakBeforeBraces: Allman BreakBeforeBraces: Allman

View File

@@ -102,7 +102,7 @@ else ()
target_compile_options(${PROJECT_NAME} PRIVATE -O0 -pg) target_compile_options(${PROJECT_NAME} PRIVATE -O0 -pg)
else () else ()
set(CMAKE_BUILD_TYPE "Release") set(CMAKE_BUILD_TYPE "Release")
target_compile_options(${PROJECT_NAME} PRIVATE -O2) target_compile_options(${PROJECT_NAME} PRIVATE -Os)
endif () endif ()
target_link_libraries(${PROJECT_NAME} PRIVATE m) target_link_libraries(${PROJECT_NAME} PRIVATE m)
@@ -135,4 +135,25 @@ target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mi
message(STATUS "System: ${CMAKE_SYSTEM_NAME}") message(STATUS "System: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Project: ${PROJECT_NAME}") message(STATUS "Project: ${PROJECT_NAME}")
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}") message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}")
get_target_property(PROJECT_COMPILE_OPTIONS ${PROJECT_NAME} COMPILE_OPTIONS)
if (NOT PROJECT_COMPILE_OPTIONS)
set(PROJECT_COMPILE_OPTIONS "<none>")
endif ()
string(TOUPPER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_UPPER)
set(EFFECTIVE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
if (BUILD_TYPE_UPPER)
set(CONFIG_FLAGS_VAR "CMAKE_CXX_FLAGS_${BUILD_TYPE_UPPER}")
if (DEFINED ${CONFIG_FLAGS_VAR})
string(APPEND EFFECTIVE_CXX_FLAGS " ${${CONFIG_FLAGS_VAR}}")
endif ()
endif ()
string(STRIP "${EFFECTIVE_CXX_FLAGS}" EFFECTIVE_CXX_FLAGS)
if (EFFECTIVE_CXX_FLAGS STREQUAL "")
set(EFFECTIVE_CXX_FLAGS "<none>")
endif ()
message(STATUS "Compiler Flags: ${EFFECTIVE_CXX_FLAGS}")
message(STATUS "Target Compile Options: ${PROJECT_COMPILE_OPTIONS}")
message(STATUS "Build: ${CMAKE_BUILD_TYPE}") message(STATUS "Build: ${CMAKE_BUILD_TYPE}")

View File

@@ -105,10 +105,7 @@ namespace anm2ed::anm2
return element; return element;
} }
void Animation::serialize(XMLDocument& document, XMLElement* parent) void Animation::serialize(XMLDocument& document, XMLElement* parent) { parent->InsertEndChild(to_element(document)); }
{
parent->InsertEndChild(to_element(document));
}
std::string Animation::to_string() std::string Animation::to_string()
{ {
@@ -153,13 +150,13 @@ namespace anm2ed::anm2
if (isRootTransform) if (isRootTransform)
{ {
auto root = rootAnimation.frame_generate(t, anm2::ROOT); auto root = rootAnimation.frame_generate(t, ROOT);
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
} }
for (auto& [id, layerAnimation] : layerAnimations) for (auto& [id, layerAnimation] : layerAnimations)
{ {
auto frame = layerAnimation.frame_generate(t, anm2::LAYER); auto frame = layerAnimation.frame_generate(t, LAYER);
if (frame.size == vec2() || !frame.isVisible) continue; if (frame.size == vec2() || !frame.isVisible) continue;

View File

@@ -131,6 +131,8 @@ namespace anm2ed::anm2
finalIndex -= numDeletedBefore; finalIndex -= numDeletedBefore;
} }
animation.frameNum = animation.length();
return finalIndex; return finalIndex;
} }

84
src/anm2/anm2_type.h Normal file
View File

@@ -0,0 +1,84 @@
#pragma once
#include "icon.h"
#include <glm/glm/vec2.hpp>
#include <glm/glm/vec3.hpp>
#include <glm/glm/vec4.hpp>
namespace anm2ed::anm2
{
constexpr auto ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f);
constexpr auto ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f);
constexpr auto ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f);
constexpr auto LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f);
constexpr auto LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f);
constexpr auto LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f);
constexpr auto NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f);
constexpr auto NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f);
constexpr auto NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f);
constexpr auto TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f);
constexpr auto TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f);
constexpr auto TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f);
#define TYPE_LIST \
X(NONE, "", "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \
X(ROOT, "Root", "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \
X(LAYER, "Layer", "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, LAYER_COLOR_HOVERED) \
X(NULL_, "Null", "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, NULL_COLOR_HOVERED) \
X(TRIGGER, "Triggers", "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \
TRIGGER_COLOR_HOVERED)
enum Type
{
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) symbol,
TYPE_LIST
#undef X
};
constexpr const char* TYPE_STRINGS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string,
TYPE_LIST
#undef X
};
constexpr const char* TYPE_ITEM_STRINGS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString,
TYPE_LIST
#undef X
};
constexpr resource::icon::Type TYPE_ICONS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon,
TYPE_LIST
#undef X
};
constexpr glm::vec4 TYPE_COLOR[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color,
TYPE_LIST
#undef X
};
constexpr glm::vec4 TYPE_COLOR_ACTIVE[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive,
TYPE_LIST
#undef X
};
constexpr glm::vec4 TYPE_COLOR_HOVERED[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered,
TYPE_LIST
#undef X
};
enum ChangeType
{
ADD,
SUBTRACT,
ADJUST
};
}

View File

@@ -114,19 +114,13 @@ namespace anm2ed::anm2
return xml::document_to_string(document); return xml::document_to_string(document);
} }
void Frame::shorten() void Frame::shorten() { delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); }
{
delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX);
}
void Frame::extend() void Frame::extend() { delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); }
{
delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX);
}
bool Frame::is_visible(Type type) bool Frame::is_visible(Type type)
{ {
if (type == anm2::TRIGGER) if (type == TRIGGER)
return isVisible && eventID > -1; return isVisible && eventID > -1;
else else
return isVisible; return isVisible;

View File

@@ -4,10 +4,7 @@
#include <string> #include <string>
#include <tinyxml2/tinyxml2.h> #include <tinyxml2/tinyxml2.h>
#include <glm/glm/vec2.hpp> #include "anm2_type.h"
#include <glm/glm/vec3.hpp>
#include <glm/glm/vec4.hpp>
#include "types.h" #include "types.h"
namespace anm2ed::anm2 namespace anm2ed::anm2
@@ -15,39 +12,6 @@ namespace anm2ed::anm2
constexpr auto FRAME_DELAY_MIN = 1; constexpr auto FRAME_DELAY_MIN = 1;
constexpr auto FRAME_DELAY_MAX = 100000; constexpr auto FRAME_DELAY_MAX = 100000;
#define TYPE_LIST \
X(NONE, "None", "None") \
X(ROOT, "Root", "RootAnimation") \
X(LAYER, "Layer", "LayerAnimation") \
X(NULL_, "Null", "NullAnimation") \
X(TRIGGER, "Trigger", "Triggers")
enum Type
{
#define X(symbol, string, animationString) symbol,
TYPE_LIST
#undef X
};
constexpr const char* TYPE_STRINGS[] = {
#define X(symbol, string, animationString) string,
TYPE_LIST
#undef X
};
constexpr const char* TYPE_ANIMATION_STRINGS[] = {
#define X(symbol, string, animationString) animationString,
TYPE_LIST
#undef X
};
enum ChangeType
{
ADD,
SUBTRACT,
ADJUST
};
#define MEMBERS \ #define MEMBERS \
X(isVisible, bool, true) \ X(isVisible, bool, true) \
X(isInterpolated, bool, false) \ X(isInterpolated, bool, false) \

View File

@@ -24,7 +24,7 @@ namespace anm2ed::anm2
XMLElement* Item::to_element(XMLDocument& document, Type type, int id) XMLElement* Item::to_element(XMLDocument& document, Type type, int id)
{ {
auto element = document.NewElement(TYPE_ANIMATION_STRINGS[type]); auto element = document.NewElement(TYPE_ITEM_STRINGS[type]);
if (type == LAYER) element->SetAttribute("LayerId", id); if (type == LAYER) element->SetAttribute("LayerId", id);
if (type == NULL_) element->SetAttribute("NullId", id); if (type == NULL_) element->SetAttribute("NullId", id);

View File

@@ -2,6 +2,9 @@
#ifdef _WIN32 #ifdef _WIN32
#include <window.h> #include <window.h>
#elif __unix__
#else
#include "toast.h"
#endif #endif
#include <format> #include <format>
@@ -57,8 +60,10 @@ namespace anm2ed
{ {
#ifdef _WIN32 #ifdef _WIN32
ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL); ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL);
#else #elif __unix__
system(std::format("xdg-open \"{}\" &", path).c_str()); system(std::format("xdg-open \"{}\" &", path).c_str());
#else
toasts.info("Operation not supported.");
#endif #endif
} }

View File

@@ -9,9 +9,7 @@ namespace anm2ed::dialog
#if defined(_WIN32) #if defined(_WIN32)
#define EXECUTABLE_FILTER {"Executable", "exe"} #define EXECUTABLE_FILTER {"Executable", "exe"}
#else #else
#define EXECUTABLE_FILTER \ #define EXECUTABLE_FILTER {"Executable", "*"}
{ \
}
#endif #endif
#define FILTER_LIST \ #define FILTER_LIST \

View File

@@ -74,12 +74,9 @@ namespace anm2ed::imgui
} }
auto isRequested = i == manager.pendingSelected; auto isRequested = i == manager.pendingSelected;
auto font = isDirty ? font::ITALICS : font::REGULAR; auto font = isDirty ? font::ITALICS : font::REGULAR;
auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get().string()) auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get().string())
: document.filename_get().string(); : document.filename_get().string();
auto label = std::format("{}###Document{}", string, i); auto label = std::format("{}###Document{}", string, i);
auto flags = isDirty ? ImGuiTabItemFlags_UnsavedDocument : 0; auto flags = isDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
@@ -89,7 +86,9 @@ namespace anm2ed::imgui
if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags)) if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags))
{ {
manager.set(i); manager.set(i);
if (isRequested) manager.pendingSelected = -1; if (isRequested) manager.pendingSelected = -1;
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
ImGui::PopFont(); ImGui::PopFont();

View File

@@ -166,10 +166,7 @@ namespace anm2ed::imgui
return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count; return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count;
} }
ImVec2 widget_size_with_row_get(int count, float width) ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); }
{
return ImVec2(row_widget_width_get(count, width), 0);
}
float footer_height_get(int itemCount) float footer_height_get(int itemCount)
{ {
@@ -265,17 +262,13 @@ namespace anm2ed::imgui
return ImGui::Shortcut(string_to_chord(string), flags); return ImGui::Shortcut(string_to_chord(string), flags);
} }
MultiSelectStorage::MultiSelectStorage() MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; }
{
internal.AdapterSetItemSelected = external_storage_set;
}
void MultiSelectStorage::start(size_t size) void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags)
{ {
internal.UserData = this; internal.UserData = this;
auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect2d, auto io = ImGui::BeginMultiSelect(flags, this->size(), size);
this->size(), size);
internal.ApplyRequests(io); internal.ApplyRequests(io);
} }
@@ -299,10 +292,7 @@ namespace anm2ed::imgui
isJustOpened = true; isJustOpened = true;
} }
bool PopupHelper::is_open() bool PopupHelper::is_open() { return isOpen; }
{
return isOpen;
}
void PopupHelper::trigger() void PopupHelper::trigger()
{ {
@@ -322,13 +312,7 @@ namespace anm2ed::imgui
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0)); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0));
} }
void PopupHelper::end() void PopupHelper::end() { isJustOpened = false; }
{
isJustOpened = false;
}
void PopupHelper::close() void PopupHelper::close() { isOpen = false; }
{
isOpen = false;
}
} }

View File

@@ -190,7 +190,9 @@ namespace anm2ed::imgui
using std::set<int>::erase; using std::set<int>::erase;
MultiSelectStorage(); MultiSelectStorage();
void start(size_t); void start(size_t, ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_BoxSelect2d |
ImGuiMultiSelectFlags_ClearOnEscape |
ImGuiMultiSelectFlags_ScopeWindow);
void finish(); void finish();
}; };

View File

@@ -1,13 +1,23 @@
#include "taskbar.h" #include "taskbar.h"
#include <imgui/imgui.h> #include <array>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <ranges> #include <ranges>
#include <imgui/imgui.h>
#include "math_.h" #include "math_.h"
#include "render.h" #include "render.h"
#include "shader.h" #include "shader.h"
#include "toast.h"
#include "types.h" #include "types.h"
#include "icon.h"
using namespace anm2ed::resource; using namespace anm2ed::resource;
using namespace anm2ed::types; using namespace anm2ed::types;
using namespace anm2ed::canvas; using namespace anm2ed::canvas;
@@ -16,9 +26,110 @@ using namespace glm;
namespace anm2ed::imgui namespace anm2ed::imgui
{ {
Taskbar::Taskbar() : generate(vec2()) #ifdef __unix__
namespace
{ {
constexpr std::array<int, 7> ICON_SIZES{16, 24, 32, 48, 64, 128, 256};
bool ensure_parent_directory_exists(const std::filesystem::path& path)
{
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec)
{
toasts.warning(std::format("Could not create directory for {} ({})", path.string(), ec.message()));
return false;
} }
return true;
}
bool write_binary_blob(const std::filesystem::path& path, const std::uint8_t* data, size_t size)
{
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not open {} for writing", path.string()));
return false;
}
file.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
return true;
}
bool run_command_checked(const std::string& command, const std::string& description)
{
auto result = std::system(command.c_str());
if (result != 0)
{
toasts.warning(std::format("{} failed (exit code {})", description, result));
return false;
}
return true;
}
bool install_icon_set(const std::string& context, const std::string& iconName, const std::filesystem::path& path)
{
bool success = true;
for (auto size : ICON_SIZES)
{
auto command = std::format("xdg-icon-resource install --noupdate --novendor --context {} --size {} \"{}\" {}",
context, size, path.string(), iconName);
success &= run_command_checked(command, std::format("Install {} icon ({}px)", iconName, size));
}
return success;
}
bool uninstall_icon_set(const std::string& context, const std::string& iconName)
{
bool success = true;
for (auto size : ICON_SIZES)
{
auto command =
std::format("xdg-icon-resource uninstall --noupdate --context {} --size {} {}", context, size, iconName);
success &= run_command_checked(command, std::format("Remove {} icon ({}px)", iconName, size));
}
return success;
}
bool remove_file_if_exists(const std::filesystem::path& path)
{
std::error_code ec;
if (!std::filesystem::exists(path, ec)) return true;
std::filesystem::remove(path, ec);
if (ec)
{
toasts.warning(std::format("Could not remove {} ({})", path.string(), ec.message()));
return false;
}
return true;
}
}
constexpr auto MIME_TYPE = R"(<?xml version="1.0" encoding="utf-8"?>
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-anm2+xml">
<!--Created automatically by update-mime-database. DO NOT EDIT!-->
<comment>Anm2 Animation</comment>
<glob pattern="*.anm2"/>
</mime-type>
)";
constexpr auto DESKTOP_ENTRY_FORMAT = R"([Desktop Entry]
Type=Application
Name=Anm2Ed
Icon=anm2ed
Comment=Animation editor for .anm2 files
Exec={}
Terminal=false
Categories=Graphics;Development;
MimeType=application/x-anm2+xml;
)";
#endif
Taskbar::Taskbar() : generate(vec2()) {}
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
{ {
@@ -119,6 +230,141 @@ namespace anm2ed::imgui
configurePopup.open(); configurePopup.open();
} }
ImGui::Separator();
if (ImGui::MenuItem("Associate .anm2 Files with Editor", nullptr, false,
!isAnm2Association || !isAbleToAssociateAnm2))
{
#ifdef _WIN32
#elif __unix__
auto cache_icons = []()
{
auto programIconPath = std::filesystem::path(filesystem::path_icon_get());
auto fileIconPath = std::filesystem::path(filesystem::path_icon_file_get());
auto iconBytes = std::size(resource::icon::PROGRAM);
bool isSuccess = write_binary_blob(programIconPath, resource::icon::PROGRAM, iconBytes) &&
write_binary_blob(fileIconPath, resource::icon::PROGRAM, iconBytes);
if (isSuccess)
{
isSuccess = install_icon_set("apps", "anm2ed", programIconPath) &&
install_icon_set("mimetypes", "application-x-anm2+xml", fileIconPath) &&
run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache");
}
remove_file_if_exists(programIconPath);
remove_file_if_exists(fileIconPath);
if (isSuccess) toasts.info("Cached program and file icons.");
return isSuccess;
};
auto register_mime = []()
{
auto path = std::filesystem::path(filesystem::path_mime_get());
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not write .anm2 MIME type: {}", path.string()));
return false;
}
file << MIME_TYPE;
file.close();
toasts.info(std::format("Wrote .anm2 MIME type to: {}", path.string()));
auto mimeRoot = path.parent_path().parent_path();
auto command = std::format("update-mime-database \"{}\"", mimeRoot.string());
return run_command_checked(command, "Update MIME database");
};
auto register_desktop_entry = []()
{
auto path = std::filesystem::path(filesystem::path_application_get());
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not write desktop entry: {}", path.string()));
return false;
}
auto desktopEntry = std::format(DESKTOP_ENTRY_FORMAT, filesystem::path_executable_get());
file << desktopEntry;
file.close();
toasts.info(std::format("Wrote desktop entry to: {}", path.string()));
auto desktopDir = path.parent_path();
auto desktopUpdate =
std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string());
auto desktopFileName = path.filename().string();
auto setDefault = std::format("xdg-mime default {} application/x-anm2+xml",
desktopFileName.empty() ? path.string() : desktopFileName);
auto databaseUpdated = run_command_checked(desktopUpdate, "Update desktop database");
auto defaultRegistered = run_command_checked(setDefault, "Set default handler for .anm2");
return databaseUpdated && defaultRegistered;
};
auto iconsCached = cache_icons();
auto mimeRegistered = register_mime();
auto desktopRegistered = register_desktop_entry();
isAnm2Association = iconsCached && mimeRegistered && desktopRegistered;
if (isAnm2Association)
toasts.info("Associated .anm2 files with the editor.");
else
toasts.warning("Association incomplete. Please review the warnings above.");
#endif
}
ImGui::SetItemTooltip(
"Associate .anm2 files with the application (i.e., clicking on them in a file explorer will "
"open the application).");
if (ImGui::MenuItem("Remove .anm2 File Association", nullptr, false,
isAnm2Association || !isAbleToAssociateAnm2))
{
#ifdef _WIN32
#elif __unix__
{
auto iconsRemoved =
uninstall_icon_set("apps", "anm2ed") && uninstall_icon_set("mimetypes", "application-x-anm2+xml") &&
run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache");
if (iconsRemoved)
toasts.info("Removed cached icons.");
else
toasts.warning("Could not remove all cached icons.");
}
{
auto path = std::filesystem::path(filesystem::path_mime_get());
auto removed = remove_file_if_exists(path);
if (removed) toasts.info(std::format("Removed .anm2 MIME type: {}", path.string()));
auto mimeRoot = path.parent_path().parent_path();
run_command_checked(std::format("update-mime-database \"{}\"", mimeRoot.string()), "Update MIME database");
}
{
auto path = std::filesystem::path(filesystem::path_application_get());
if (remove_file_if_exists(path)) toasts.info(std::format("Removed desktop entry: {}", path.string()));
auto desktopDir = path.parent_path();
run_command_checked(
std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()),
"Update desktop database");
}
#endif
isAnm2Association = false;
}
ImGui::SetItemTooltip("Unassociate .anm2 files with the application.");
ImGui::EndMenu(); ImGui::EndMenu();
} }
@@ -546,7 +792,7 @@ namespace anm2ed::imgui
if (dialogType == dialog::PNG_DIRECTORY_SET) if (dialogType == dialog::PNG_DIRECTORY_SET)
dialog.folder_open(dialogType); dialog.folder_open(dialogType);
else else
dialog.file_open(dialogType); dialog.file_save(dialogType);
} }
ImGui::SameLine(); ImGui::SameLine();
input_text_string(type == render::PNGS ? "Directory" : "Path", &path); input_text_string(type == render::PNGS ? "Directory" : "Path", &path);
@@ -581,11 +827,16 @@ namespace anm2ed::imgui
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Raw", &isRaw); ImGui::Checkbox("Raw", &isRaw);
ImGui::SetItemTooltip("Record only the layers of the animation."); ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds.");
ImGui::SameLine();
ImGui::Checkbox("Sound", &settings.timelineIsSound);
ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window.\nThe "
"output animation will use the played sounds.");
if (ImGui::Button("Render", widgetSize)) if (ImGui::Button("Render", widgetSize))
{ {
manager.isRecording = true;
manager.isRecordingStart = true; manager.isRecordingStart = true;
playback.time = start; playback.time = start;
playback.isPlaying = true; playback.isPlaying = true;

View File

@@ -2,6 +2,7 @@
#include "canvas.h" #include "canvas.h"
#include "dialog.h" #include "dialog.h"
#include "filesystem_.h"
#include "imgui_.h" #include "imgui_.h"
#include "manager.h" #include "manager.h"
#include "resources.h" #include "resources.h"
@@ -20,6 +21,15 @@ namespace anm2ed::imgui
PopupHelper aboutPopup{PopupHelper("About")}; PopupHelper aboutPopup{PopupHelper("About")};
Settings editSettings{}; Settings editSettings{};
int selectedShortcut{-1}; int selectedShortcut{-1};
#if defined(_WIN32) || defined(__unix__)
bool isAbleToAssociateAnm2 = true;
#else
bool isAbleToAssociateAnm2 = false;
#endif
bool isAnm2Association = std::filesystem::exists(util::filesystem::path_application_get());
bool isQuittingMode{}; bool isQuittingMode{};
public: public:

View File

@@ -9,7 +9,8 @@ using namespace anm2ed::types;
namespace anm2ed::imgui namespace anm2ed::imgui
{ {
constexpr auto LIFETIME = 3.0f; constexpr auto LIFETIME = 4.0f;
constexpr auto FADE_THRESHOLD = 1.0f;
Toast::Toast(const std::string& message) Toast::Toast(const std::string& message)
{ {
@@ -30,8 +31,6 @@ namespace anm2ed::imgui
{ {
Toast& toast = toasts[i]; Toast& toast = toasts[i];
toast.lifetime -= ImGui::GetIO().DeltaTime;
if (toast.lifetime <= 0.0f) if (toast.lifetime <= 0.0f)
{ {
toasts.erase(toasts.begin() + i); toasts.erase(toasts.begin() + i);
@@ -39,7 +38,9 @@ namespace anm2ed::imgui
continue; continue;
} }
auto alpha = toast.lifetime / LIFETIME; toast.lifetime -= ImGui::GetIO().DeltaTime;
auto alpha = toast.lifetime <= FADE_THRESHOLD ? toast.lifetime / FADE_THRESHOLD : 1.0f;
borderColor.w = alpha; borderColor.w = alpha;
textColor.w = alpha; textColor.w = alpha;
@@ -57,6 +58,8 @@ namespace anm2ed::imgui
{ {
ImGui::TextUnformatted(toast.message.c_str()); ImGui::TextUnformatted(toast.message.c_str());
position.y -= ImGui::GetWindowSize().y + ImGui::GetStyle().ItemSpacing.y; position.y -= ImGui::GetWindowSize().y + ImGui::GetStyle().ItemSpacing.y;
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) toast.lifetime = 0.0f;
} }
ImGui::End(); ImGui::End();
ImGui::PopStyleColor(2); ImGui::PopStyleColor(2);

View File

@@ -42,51 +42,22 @@ namespace anm2ed::imgui
auto& isSound = settings.timelineIsSound; auto& isSound = settings.timelineIsSound;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
if (isSound && !anm2.content.sounds.empty()) if (!anm2.content.sounds.empty() && isSound)
if (auto animation = document.animation_get(); animation) {
if (animation->triggers.isVisible && !isOnlyShowLayers) if (auto animation = document.animation_get();
animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
{
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER);
trigger.is_visible(anm2::TRIGGER)) trigger.is_visible(anm2::TRIGGER))
if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(); if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(mixer);
}
}
document.reference.frameTime = playback.time; document.reference.frameTime = playback.time;
} }
if (manager.isRecording) if (manager.isRecording)
{ {
if (manager.isRecordingStart)
{
if (settings.renderIsRawAnimation)
{
savedSettings = settings;
settings.previewBackgroundColor = vec4();
settings.previewIsGrid = false;
settings.previewIsAxes = false;
settings.timelineIsOnlyShowLayers = true;
savedZoom = zoom;
savedPan = pan;
if (auto animation = document.animation_get())
{
auto rect = animation->rect(isRootTransform);
size = vec2(rect.z, rect.w) * scale;
set_to_rect(zoom, pan, rect);
}
isSizeTrySet = false;
bind();
viewport_set();
clear(settings.previewBackgroundColor);
unbind();
}
manager.isRecordingStart = false;
return; // Need to wait an additional frame. Kind of hacky, but oh well.
}
auto pixels = pixels_get(); auto pixels = pixels_get();
renderFrames.push_back(Texture(pixels.data(), size)); renderFrames.push_back(Texture(pixels.data(), size));
@@ -120,7 +91,7 @@ namespace anm2ed::imgui
} }
else else
{ {
if (animation_render(ffmpegPath, path, renderFrames, (render::Type)type, size, anm2.info.fps)) if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps))
toasts.info(std::format("Exported rendered animation to: {}", path)); toasts.info(std::format("Exported rendered animation to: {}", path));
else else
toasts.warning(std::format("Could not output rendered animation: {}", path)); toasts.warning(std::format("Could not output rendered animation: {}", path));
@@ -133,12 +104,49 @@ namespace anm2ed::imgui
settings = savedSettings; settings = savedSettings;
isSizeTrySet = true; isSizeTrySet = true;
if (settings.timelineIsSound) audioStream.capture_end(mixer);
playback.isPlaying = false; playback.isPlaying = false;
playback.isFinished = false; playback.isFinished = false;
manager.isRecording = false; manager.isRecording = false;
manager.progressPopup.close(); manager.progressPopup.close();
} }
} }
if (manager.isRecordingStart)
{
savedSettings = settings;
if (settings.timelineIsSound) audioStream.capture_begin(mixer);
if (settings.renderIsRawAnimation)
{
settings.previewBackgroundColor = vec4();
settings.previewIsGrid = false;
settings.previewIsAxes = false;
settings.timelineIsOnlyShowLayers = true;
savedZoom = zoom;
savedPan = pan;
if (auto animation = document.animation_get())
{
if (auto rect = animation->rect(isRootTransform); rect != vec4(-1.0f))
{
size_set(vec2(rect.w, rect.z) * scale);
set_to_rect(zoom, pan, rect);
}
}
isSizeTrySet = false;
bind();
clear(settings.previewBackgroundColor);
unbind();
}
manager.isRecordingStart = false;
manager.isRecording = true;
}
} }
void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources) void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources)
@@ -270,8 +278,8 @@ namespace anm2ed::imgui
auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto cursorScreenPos = ImGui::GetCursorScreenPos();
if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail())); if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail()));
bind();
viewport_set(); viewport_set();
bind();
clear(backgroundColor); clear(backgroundColor);
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor); if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
@@ -412,7 +420,7 @@ namespace anm2ed::imgui
isPreviewHovered = ImGui::IsItemHovered(); isPreviewHovered = ImGui::IsItemHovered();
if (animation && animation->triggers.isVisible && !isOnlyShowLayers) if (animation && animation->triggers.isVisible && !isOnlyShowLayers && !manager.isRecording)
{ {
if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER);
trigger.isVisible && trigger.eventID > -1) trigger.isVisible && trigger.eventID > -1)

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "audio_stream.h"
#include "canvas.h" #include "canvas.h"
#include "manager.h" #include "manager.h"
#include "resources.h" #include "resources.h"
@@ -9,6 +10,8 @@ namespace anm2ed::imgui
{ {
class AnimationPreview : public Canvas class AnimationPreview : public Canvas
{ {
MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
AudioStream audioStream = AudioStream(mixer);
bool isPreviewHovered{}; bool isPreviewHovered{};
bool isSizeTrySet{true}; bool isSizeTrySet{true};
Settings savedSettings{}; Settings savedSettings{};

View File

@@ -85,7 +85,7 @@ namespace anm2ed::imgui
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
selection.start(anm2.content.spritesheets.size()); selection.start(anm2.content.spritesheets.size(), ImGuiMultiSelectFlags_ClearOnEscape);
for (auto& [id, spritesheet] : anm2.content.spritesheets) for (auto& [id, spritesheet] : anm2.content.spritesheets)
{ {
@@ -168,16 +168,16 @@ namespace anm2ed::imgui
context_menu(); context_menu();
} }
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopID(); ImGui::PopID();
} }
selection.finish();
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
context_menu(); context_menu();
selection.finish();
} }
ImGui::EndChild(); ImGui::EndChild();

View File

@@ -12,26 +12,8 @@ using namespace glm;
namespace anm2ed::imgui namespace anm2ed::imgui
{ {
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 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_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_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 FRAME_MULTIPLE_OVERLAY_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.05f);
constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f; constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f;
@@ -47,11 +29,19 @@ namespace anm2ed::imgui
- Press {} to extend the selected frame, by one frame. - Press {} to extend the selected frame, by one frame.
- Hold Alt while clicking a non-trigger frame to toggle interpolation.)"; - Hold Alt while clicking a non-trigger frame to toggle interpolation.)";
void Timeline::context_menu(Document& document, Settings& settings, Clipboard& clipboard) void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& playback = document.playback;
auto& reference = document.reference;
auto animation = document.animation_get();
style = ImGui::GetStyle();
auto context_menu = [&]()
{ {
auto& hoveredFrame = document.hoveredFrame; auto& hoveredFrame = document.hoveredFrame;
auto& anm2 = document.anm2; auto& anm2 = document.anm2;
auto& reference = document.reference;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
@@ -101,56 +91,29 @@ namespace anm2ed::imgui
{ {
if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut(); if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut();
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy(); if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy();
if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste(); if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste();
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
} };
void Timeline::item_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, auto item_child = [&](anm2::Type type, int id, int& index)
Resources& resources, Clipboard& clipboard, anm2::Type type, int id, int& index)
{ {
auto& anm2 = document.anm2; auto& anm2 = document.anm2;
auto& reference = document.reference;
auto item = animation ? animation->item_get(type, id) : nullptr; auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false; auto isVisible = item ? item->isVisible : false;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false;
auto isActive = reference.itemType == type && reference.itemID == id; 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("#{} {} (Spritesheet: #{})", id, anm2.content.layers.at(id).name,
anm2.content.layers[id].spritesheetID);
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;
}
auto label = type == anm2::LAYER ? std::format(anm2::LAYER_FORMAT, id, anm2.content.layers.at(id).name,
anm2.content.layers[id].spritesheetID)
: type == anm2::NULL_ ? std::format(anm2::NULL_FORMAT, id, anm2.content.nulls[id].name)
: anm2::TYPE_STRINGS[type];
auto icon = anm2::TYPE_ICONS[type];
auto color = to_imvec4(isActive ? anm2::TYPE_COLOR_ACTIVE[type] : anm2::TYPE_COLOR[type]);
color = !isVisible ? to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER) : color; color = !isVisible ? to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER) : color;
ImGui::PushStyleColor(ImGuiCol_ChildBg, color); ImGui::PushStyleColor(ImGuiCol_ChildBg, color);
@@ -173,10 +136,10 @@ namespace anm2ed::imgui
switch (type) switch (type)
{ {
case anm2::LAYER: case anm2::LAYER:
manager.layer_properties_open(id); // Handled in layers.cpp manager.layer_properties_open(id);
break; break;
case anm2::NULL_: case anm2::NULL_:
manager.null_properties_open(id); // Handled in layers.cpp manager.null_properties_open(id);
default: default:
break; break;
} }
@@ -189,21 +152,23 @@ namespace anm2ed::imgui
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextUnformatted(label.c_str()); ImGui::TextUnformatted(label.c_str());
anm2::Item* item = animation->item_get(type, id); anm2::Item* itemPtr = animation->item_get(type, id);
bool& isVisible = item->isVisible; bool& itemVisible = itemPtr->isVisible;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2());
ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, ImGui::SetCursorPos(
ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE; int visibleIcon = itemVisible ? icon::VISIBLE : icon::INVISIBLE;
if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get()))
DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, isVisible = !isVisible); DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, itemVisible = !itemVisible);
ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show."); ImGui::SetItemTooltip(itemVisible ? "The item is shown. Press to hide."
: "The item is hidden. Press to show.");
if (type == anm2::NULL_) if (type == anm2::NULL_)
{ {
@@ -228,7 +193,8 @@ namespace anm2ed::imgui
auto cursorPos = ImGui::GetCursorPos(); auto cursorPos = ImGui::GetCursorPos();
auto& isShowUnused = settings.timelineIsShowUnused; auto& isShowUnused = settings.timelineIsShowUnused;
ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, ImGui::SetCursorPos(
ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4());
@@ -242,16 +208,16 @@ namespace anm2ed::imgui
ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide." ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide."
: "Unused layers/nulls are hidden. Press to show."); : "Unused layers/nulls are hidden. Press to show.");
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& showLayersOnly = settings.timelineIsOnlyShowLayers;
auto layersIcon = isOnlyShowLayers ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; auto layersIcon = showLayersOnly ? icon::SHOW_LAYERS : icon::HIDE_LAYERS;
ImGui::SetCursorPos( ImGui::SetCursorPos(
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get()))
isOnlyShowLayers = !isOnlyShowLayers; showLayersOnly = !showLayersOnly;
ImGui::SetItemTooltip(isOnlyShowLayers ? "Only layers are visible. Press to show all items." ImGui::SetItemTooltip(showLayersOnly ? "Only layers are visible. Press to show all items."
: "All items are visible. Press to only show layers."); : "All items are visible. Press to only show layers.");
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -261,7 +227,8 @@ namespace anm2ed::imgui
ImGui::BeginDisabled(); ImGui::BeginDisabled();
ImGui::Text("(?)"); ImGui::Text("(?)");
ImGui::SetItemTooltip("%s", std::format(HELP_FORMAT, settings.shortcutNextFrame, settings.shortcutPreviousFrame, ImGui::SetItemTooltip("%s",
std::format(HELP_FORMAT, settings.shortcutNextFrame, settings.shortcutPreviousFrame,
settings.shortcutShortenFrame, settings.shortcutExtendFrame) settings.shortcutShortenFrame, settings.shortcutExtendFrame)
.c_str()); .c_str());
ImGui::EndDisabled(); ImGui::EndDisabled();
@@ -271,13 +238,10 @@ namespace anm2ed::imgui
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
index++; index++;
} };
void Timeline::items_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, auto items_child = [&]()
Resources& resources, Clipboard& clipboard)
{ {
auto& reference = document.reference;
auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y); auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders)) if (ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders))
@@ -305,7 +269,7 @@ namespace anm2ed::imgui
{ {
ImGui::TableNextRow(); ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(0);
item_child(manager, document, animation, settings, resources, clipboard, type, id, index); item_child(type, id, index);
}; };
item_child_row(anm2::NONE); item_child_row(anm2::NONE);
@@ -316,17 +280,13 @@ namespace anm2ed::imgui
for (auto& id : animation->layerOrder) for (auto& id : animation->layerOrder)
{ {
if (anm2::Item* item = animation->item_get(anm2::LAYER, id); item) if (!settings.timelineIsShowUnused && animation->layerAnimations[id].frames.empty()) continue;
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
item_child_row(anm2::LAYER, id); item_child_row(anm2::LAYER, id);
} }
for (auto& id : animation->nullAnimations | std::views::keys) for (auto& [id, nullAnimation] : animation->nullAnimations)
{ {
if (anm2::Item* item = animation->item_get(anm2::NULL_, id); item) if (!settings.timelineIsShowUnused && nullAnimation.frames.empty()) continue;
if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
item_child_row(anm2::NULL_, id); item_child_row(anm2::NULL_, id);
} }
@@ -372,7 +332,7 @@ namespace anm2ed::imgui
DOCUMENT_EDIT(document, "Remove Item", Document::ITEMS, remove()); DOCUMENT_EDIT(document, "Remove Item", Document::ITEMS, remove());
} }
set_item_tooltip_shortcut("Remove the selected items from the animation.", settings.shortcutRemove); set_item_tooltip_shortcut("Remove the selected item(s) from the animation.", settings.shortcutRemove);
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
@@ -381,68 +341,34 @@ namespace anm2ed::imgui
ImGui::PopStyleVar(); ImGui::PopStyleVar();
} }
ImGui::EndChild(); ImGui::EndChild();
} };
void Timeline::frame_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources, auto frame_child = [&](anm2::Type type, int id, int& index, float width)
Clipboard& clipboard, anm2::Type type, int id, int& index, float width)
{ {
auto& anm2 = document.anm2; auto& anm2 = document.anm2;
auto& playback = document.playback;
auto& reference = document.reference;
auto& hoveredFrame = document.hoveredFrame; auto& hoveredFrame = document.hoveredFrame;
auto item = animation ? animation->item_get(type, id) : nullptr; auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false; auto isVisible = item ? item->isVisible : false;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false;
auto color = to_imvec4(anm2::TYPE_COLOR[type]);
auto colorActive = to_imvec4(anm2::TYPE_COLOR_ACTIVE[type]);
auto colorHovered = to_imvec4(anm2::TYPE_COLOR_HOVERED[type]);
auto colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER);
auto colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER);
auto colorHoveredHidden = to_imvec4(to_vec4(colorHidden) * COLOR_HIDDEN_MULTIPLIER);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
auto childSize = ImVec2(width, ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); 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::PopStyleVar(2);
ImGui::PushID(index); 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::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2());
if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders))
@@ -452,7 +378,6 @@ namespace anm2ed::imgui
auto framesSize = ImVec2(frameSize.x * length, frameSize.y); auto framesSize = ImVec2(frameSize.x * length, frameSize.y);
auto cursorPos = ImGui::GetCursorPos(); auto cursorPos = ImGui::GetCursorPos();
auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto cursorScreenPos = ImGui::GetCursorScreenPos();
auto imageSize = vec2(ImGui::GetTextLineHeight());
auto border = ImGui::GetStyle().FrameBorderSize; auto border = ImGui::GetStyle().FrameBorderSize;
auto borderLineLength = frameSize.y / 5; auto borderLineLength = frameSize.y / 5;
auto scrollX = ImGui::GetScrollX(); auto scrollX = ImGui::GetScrollX();
@@ -515,56 +440,33 @@ namespace anm2ed::imgui
} }
else if (animation) else if (animation)
{ {
anm2::Reference itemReference = {reference.animationIndex, type, id}; anm2::Reference itemReference{reference.animationIndex, type, id};
if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(0)) reference = itemReference;
for (int i = frameMin; i < frameMax; i++) ImGui::PushStyleColor(ImGuiCol_ButtonActive, isVisible ? colorActive : colorActiveHidden);
{ ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isVisible ? colorHovered : colorHoveredHidden);
auto frameScreenPos = ImVec2(cursorScreenPos.x + (frameSize.x * i), cursorScreenPos.y);
if (i % FRAME_MULTIPLE == 0) ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
{
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); float frameTime{};
auto frameTime = 0;
anm2::Reference baseReference = {reference.animationIndex, reference.itemType, reference.itemID,
reference.frameIndex};
for (auto [i, frame] : std::views::enumerate(item->frames)) for (auto [i, frame] : std::views::enumerate(item->frames))
{ {
anm2::Reference frameReference = {reference.animationIndex, type, id, (int)i};
auto isSelected = baseReference == frameReference;
auto isFrameVisible = isVisible && frame.isVisible;
ImGui::PushID(i); ImGui::PushID(i);
auto size = ImVec2(frameSize.x * frame.delay, frameSize.y);
auto icon = type == anm2::TRIGGER ? icon::TRIGGER auto frameReference =
: frame.isInterpolated ? icon::INTERPOLATED anm2::Reference(itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i);
: icon::UNINTERPOLATED; auto isSelected = reference == frameReference;
vec2 frameMin = {frameTime * frameSize.x, cursorPos.y};
if (type == anm2::TRIGGER) vec2 frameMax = {frameMin.x + frame.delay * frameSize.x, frameMin.y + frameSize.y};
ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * frame.atFrame, cursorPos.y)); auto buttonSize = to_imvec2(frameMax - frameMin);
auto buttonPos = ImVec2(cursorPos.x + frameMin.x, cursorPos.y);
ImGui::PushStyleColor(ImGuiCol_Button, isFrameVisible ? color : colorHidden); ImGui::SetCursorPos(buttonPos);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, isFrameVisible ? colorActive : colorActiveHidden); ImGui::PushStyleColor(ImGuiCol_Button, isSelected && isVisible ? colorActive
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isFrameVisible ? colorHovered : colorHoveredHidden); : isSelected && !isVisible ? colorActiveHidden
: isVisible ? color
if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); : colorHidden);
if (ImGui::Button("##Frame Button", buttonSize))
if (ImGui::Button("##Frame Button", size))
{ {
if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt))
DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES,
frame.isInterpolated = !frame.isInterpolated);
if (type == anm2::LAYER) if (type == anm2::LAYER)
{ {
document.spritesheet.reference = anm2.content.layers[id].spritesheetID; document.spritesheet.reference = anm2.content.layers[id].spritesheetID;
@@ -572,26 +474,28 @@ namespace anm2ed::imgui
} }
reference = frameReference; reference = frameReference;
reference.frameTime = frameTime; reference.frameTime = frameTime;
if (ImGui::IsKeyDown(ImGuiMod_Alt))
DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES,
frame.isInterpolated = !frame.isInterpolated);
} }
if (ImGui::IsItemHovered()) hoveredFrame = frameReference; if (ImGui::IsItemHovered()) hoveredFrame = frameReference;
ImGui::PopStyleColor();
if (type != anm2::TRIGGER) ImGui::SameLine(); auto icon = type == anm2::TRIGGER ? icon::TRIGGER
: frame.isInterpolated ? icon::INTERPOLATED
ImGui::PopStyleColor(3); : icon::UNINTERPOLATED;
if (isSelected) ImGui::PopStyleColor(); auto iconPos = ImVec2(cursorPos.x + (frameTime * frameSize.x),
cursorPos.y + (frameSize.y / 2) - (icon_size_get().y / 2));
auto imageMin = ImVec2(ImGui::GetItemRectMin().x, ImGui::SetCursorPos(iconPos);
ImGui::GetItemRectMax().y - (ImGui::GetItemRectSize().y / 2) - (imageSize.y / 2)); ImGui::Image(resources.icons[icon].id, icon_size_get());
auto imageMax = to_imvec2(to_vec2(imageMin) + imageSize);
drawList->AddImage(resources.icons[icon].id, imageMin, imageMax);
frameTime += frame.delay; frameTime += frame.delay;
ImGui::PopID(); ImGui::PopID();
} }
context_menu(document, settings, clipboard); ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
} }
} }
ImGui::EndChild(); ImGui::EndChild();
@@ -599,14 +503,11 @@ namespace anm2ed::imgui
index++; index++;
ImGui::PopID(); ImGui::PopID();
} };
void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources, auto frames_child = [&]()
Clipboard& clipboard)
{ {
auto& anm2 = document.anm2; auto& anm2 = document.anm2;
auto& reference = document.reference;
auto& playback = document.playback;
auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15;
@@ -663,13 +564,11 @@ namespace anm2ed::imgui
{ {
ImGui::TableNextRow(); ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(0);
frame_child(document, animation, settings, resources, clipboard, type, id, index, childWidth); frame_child(type, id, index, childWidth);
}; };
frames_child_row(anm2::NONE); frames_child_row(anm2::NONE);
//hoveredFrame = anm2::REFERENCE_DEFAULT;
if (animation) if (animation)
{ {
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
@@ -678,16 +577,16 @@ namespace anm2ed::imgui
for (auto& id : animation->layerOrder) for (auto& id : animation->layerOrder)
{ {
if (auto item = animation->item_get(anm2::LAYER, id); item) if (auto itemPtr = animation->item_get(anm2::LAYER, id); itemPtr)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue; if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue;
frames_child_row(anm2::LAYER, id); frames_child_row(anm2::LAYER, id);
} }
for (auto& id : animation->nullAnimations | std::views::keys) for (auto& id : animation->nullAnimations | std::views::keys)
{ {
if (auto item = animation->item_get(anm2::NULL_, id); item) if (auto itemPtr = animation->item_get(anm2::NULL_, id); itemPtr)
if (!settings.timelineIsShowUnused && item->frames.empty()) continue; if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue;
frames_child_row(anm2::NULL_, id); frames_child_row(anm2::NULL_, id);
} }
@@ -717,7 +616,7 @@ namespace anm2ed::imgui
ImGui::PopStyleVar(); ImGui::PopStyleVar();
context_menu(document, settings, clipboard); context_menu();
} }
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -740,9 +639,9 @@ namespace anm2ed::imgui
ImGui::SameLine(); ImGui::SameLine();
auto item = document.item_get(); auto itemPtr = document.item_get();
ImGui::BeginDisabled(!item); ImGui::BeginDisabled(!itemPtr);
{ {
shortcut(settings.shortcutAdd); shortcut(settings.shortcutAdd);
if (ImGui::Button("Insert Frame", widgetSize)) if (ImGui::Button("Insert Frame", widgetSize))
@@ -752,14 +651,14 @@ namespace anm2ed::imgui
auto frame = document.frame_get(); auto frame = document.frame_get();
if (frame) if (frame)
{ {
item->frames.insert(item->frames.begin() + reference.frameIndex, *frame); itemPtr->frames.insert(itemPtr->frames.begin() + reference.frameIndex, *frame);
reference.frameIndex++; reference.frameIndex++;
} }
else if (!item->frames.empty()) else if (!itemPtr->frames.empty())
{ {
auto frame = item->frames.back(); auto lastFrame = itemPtr->frames.back();
item->frames.emplace_back(frame); itemPtr->frames.emplace_back(lastFrame);
reference.frameIndex = item->frames.size() - 1; reference.frameIndex = static_cast<int>(itemPtr->frames.size()) - 1;
} }
}; };
@@ -776,7 +675,7 @@ namespace anm2ed::imgui
{ {
auto delete_frame = [&]() auto delete_frame = [&]()
{ {
item->frames.erase(item->frames.begin() + reference.frameIndex); itemPtr->frames.erase(itemPtr->frames.begin() + reference.frameIndex);
reference.frameIndex = glm::max(-1, --reference.frameIndex); reference.frameIndex = glm::max(-1, --reference.frameIndex);
}; };
@@ -838,9 +737,9 @@ namespace anm2ed::imgui
ImGui::EndChild(); ImGui::EndChild();
ImGui::SetCursorPos(cursorPos); ImGui::SetCursorPos(cursorPos);
} };
void Timeline::popups(Document& document, anm2::Animation* animation, Settings& settings) auto popups_fn = [&]()
{ {
auto item_properties_reset = [&]() auto item_properties_reset = [&]()
{ {
@@ -851,7 +750,6 @@ namespace anm2ed::imgui
}; };
auto& anm2 = document.anm2; auto& anm2 = document.anm2;
auto& reference = document.reference;
propertiesPopup.trigger(); propertiesPopup.trigger();
@@ -889,7 +787,8 @@ namespace anm2ed::imgui
if (ImGui::BeginChild("Type Layer", size)) if (ImGui::BeginChild("Type Layer", size))
{ {
ImGui::RadioButton("Layer", &type, anm2::LAYER); ImGui::RadioButton("Layer", &type, anm2::LAYER);
ImGui::SetItemTooltip("Layers are a basic visual element in an animation, used for displaying spritesheets."); ImGui::SetItemTooltip(
"Layers are a basic visual element in an animation, used for displaying spritesheets.");
} }
ImGui::EndChild(); ImGui::EndChild();
@@ -1044,9 +943,9 @@ namespace anm2ed::imgui
if (ImGui::Button("Bake", widgetSize)) if (ImGui::Button("Bake", widgetSize))
{ {
if (auto item = document.item_get()) if (auto itemPtr = document.item_get())
DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES,
item->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation)); itemPtr->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation));
bakePopup.close(); bakePopup.close();
} }
ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected.");
@@ -1058,28 +957,19 @@ namespace anm2ed::imgui
ImGui::EndPopup(); ImGui::EndPopup();
} }
} };
void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& playback = document.playback;
auto& reference = document.reference;
auto animation = document.animation_get();
style = ImGui::GetStyle();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::Begin("Timeline", &settings.windowIsTimeline)) if (ImGui::Begin("Timeline", &settings.windowIsTimeline))
{ {
isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
frames_child(document, animation, settings, resources, clipboard); frames_child();
items_child(manager, document, animation, settings, resources, clipboard); items_child();
} }
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::End(); ImGui::End();
popups(document, animation, settings); popups_fn();
if (shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle(); if (shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle();

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "clipboard.h" #include "clipboard.h"
#include "document.h"
#include "manager.h" #include "manager.h"
#include "resources.h" #include "resources.h"
#include "settings.h" #include "settings.h"
@@ -25,14 +24,6 @@ namespace anm2ed::imgui
ImDrawList* pickerLineDrawList{}; ImDrawList* pickerLineDrawList{};
ImGuiStyle style{}; ImGuiStyle style{};
void context_menu(Document&, Settings&, Clipboard&);
void item_child(Manager&, Document&, anm2::Animation*, Settings&, Resources&, Clipboard&, anm2::Type, int, int&);
void items_child(Manager&, Document&, anm2::Animation*, Settings&, Resources&, Clipboard&);
void frame_child(Document&, anm2::Animation*, Settings&, Resources&, Clipboard&, anm2::Type, int, int&, float);
void frames_child(Document&, anm2::Animation*, Settings&, Resources&, Clipboard&);
void popups(Document&, anm2::Animation*, Settings&);
public: public:
void update(Manager&, Settings&, Resources&, Clipboard&); void update(Manager&, Settings&, Resources&, Clipboard&);
}; };

View File

@@ -1,5 +1,7 @@
#include "loader.h" #include "loader.h"
#include <cstdint>
#include <imgui/backends/imgui_impl_opengl3.h> #include <imgui/backends/imgui_impl_opengl3.h>
#include <imgui/backends/imgui_impl_sdl3.h> #include <imgui/backends/imgui_impl_sdl3.h>
@@ -8,11 +10,57 @@
#include "filesystem_.h" #include "filesystem_.h"
#include "log.h" #include "log.h"
#include "socket.h"
using namespace anm2ed::types; using namespace anm2ed::types;
using namespace anm2ed::util; using namespace anm2ed::util;
namespace anm2ed namespace anm2ed
{ {
constexpr auto SOCKET_ADDRESS = "127.0.0.1";
constexpr auto SOCKET_PORT = 11414;
namespace
{
bool socket_paths_send(Socket& socket, const std::vector<std::string>& paths)
{
uint32_t count = htonl(static_cast<uint32_t>(paths.size()));
if (!socket.send(&count, sizeof(count))) return false;
for (const auto& path : paths)
{
uint32_t length = htonl(static_cast<uint32_t>(path.size()));
if (!socket.send(&length, sizeof(length))) return false;
if (!path.empty() && !socket.send(path.data(), path.size())) return false;
}
return true;
}
std::vector<std::string> socket_paths_receive(Socket& socket)
{
uint32_t count{};
if (!socket.receive(&count, sizeof(count))) return {};
count = ntohl(count);
std::vector<std::string> paths;
paths.reserve(count);
for (uint32_t i = 0; i < count; ++i)
{
uint32_t length{};
if (!socket.receive(&length, sizeof(length))) return {};
length = ntohl(length);
std::string path(length, '\0');
if (length > 0 && !socket.receive(path.data(), length)) return {};
paths.emplace_back(std::move(path));
}
return paths;
}
}
std::string Loader::settings_path() std::string Loader::settings_path()
{ {
return filesystem::path_preferences_get() + "settings.ini"; return filesystem::path_preferences_get() + "settings.ini";
@@ -23,6 +71,45 @@ namespace anm2ed
for (int i = 1; i < argc; i++) for (int i = 1; i < argc; i++)
arguments.emplace_back(argv[i]); arguments.emplace_back(argv[i]);
Socket testSocket;
if (!testSocket.open(SERVER))
logger.warning(std::format("Failed to open socket; single instancing will not work."));
bool isPrimaryInstance = false;
if (testSocket.bind({SOCKET_ADDRESS, SOCKET_PORT}))
{
socket = std::move(testSocket);
if (!socket.listen())
logger.warning("Could not listen on socket; single instancing disabled.");
else
{
isPrimaryInstance = true;
isSocketThread = true;
logger.info(std::format("Opened socket at {}:{}", SOCKET_ADDRESS, SOCKET_PORT));
}
}
else
{
logger.info(std::format("Existing instance of program exists; passing arguments..."));
Socket clientSocket;
if (!clientSocket.open(CLIENT))
logger.warning("Could not open client socket to forward arguments.");
else if (!clientSocket.connect({SOCKET_ADDRESS, SOCKET_PORT}))
logger.warning("Could not connect to existing instance.");
else if (!socket_paths_send(clientSocket, arguments))
logger.warning("Failed to transfer arguments to existing instance.");
else
logger.info("Sent arguments to existing instance. Exiting.");
if (!isPrimaryInstance)
{
isError = true;
return;
}
}
settings = Settings(settings_path()); settings = Settings(settings_path());
if (!SDL_Init(SDL_INIT_VIDEO)) if (!SDL_Init(SDL_INIT_VIDEO))
@@ -93,10 +180,54 @@ namespace anm2ed
io.ConfigWindowsMoveFromTitleBarOnly = true; io.ConfigWindowsMoveFromTitleBarOnly = true;
ImGui::LoadIniSettingsFromDisk(settings_path().c_str()); ImGui::LoadIniSettingsFromDisk(settings_path().c_str());
if (isSocketThread)
{
isSocketRunning = true;
socketThread = std::thread(
[this]()
{
while (isSocketRunning)
{
auto client = socket.accept();
if (!client.is_valid())
{
if (!isSocketRunning) break;
continue;
}
auto paths = socket_paths_receive(client);
for (auto& path : paths)
{
if (path.empty()) continue;
SDL_Event event{};
event.type = SDL_EVENT_DROP_FILE;
event.drop.data = SDL_strdup(path.c_str());
event.drop.windowID = window ? SDL_GetWindowID(window) : 0;
SDL_PushEvent(&event);
}
}
});
}
} }
Loader::~Loader() Loader::~Loader()
{ {
if (isSocketThread)
{
isSocketRunning = false;
if (socket.is_valid())
{
Socket wakeSocket;
if (wakeSocket.open(CLIENT) && wakeSocket.connect({SOCKET_ADDRESS, SOCKET_PORT}))
socket_paths_send(wakeSocket, {});
}
socket.close();
if (socketThread.joinable()) socketThread.join();
}
if (ImGui::GetCurrentContext()) if (ImGui::GetCurrentContext())
{ {
settings.save(settings_path(), ImGui::SaveIniSettingsToMemory(nullptr)); settings.save(settings_path(), ImGui::SaveIniSettingsToMemory(nullptr));

View File

@@ -1,11 +1,14 @@
#pragma once #pragma once
#include <atomic>
#include <string> #include <string>
#include <thread>
#include <vector> #include <vector>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "settings.h" #include "settings.h"
#include "socket.h"
namespace anm2ed namespace anm2ed
{ {
@@ -14,11 +17,15 @@ namespace anm2ed
std::string settings_path(); std::string settings_path();
public: public:
Socket socket{};
std::thread socketThread{};
std::atomic_bool isSocketRunning{};
SDL_Window* window{}; SDL_Window* window{};
SDL_GLContext glContext{}; SDL_GLContext glContext{};
Settings settings; Settings settings;
std::vector<std::string> arguments; std::vector<std::string> arguments;
bool isError{}; bool isError{};
bool isSocketThread{};
Loader(int, const char**); Loader(int, const char**);
~Loader(); ~Loader();

View File

@@ -14,20 +14,9 @@ namespace anm2ed
{ {
constexpr std::size_t RECENT_LIMIT = 10; constexpr std::size_t RECENT_LIMIT = 10;
std::filesystem::path Manager::recent_files_path_get() std::filesystem::path Manager::recent_files_path_get() { return filesystem::path_preferences_get() + "recent.txt"; }
{ std::filesystem::path Manager::autosave_path_get() { return filesystem::path_preferences_get() + "autosave.txt"; }
return filesystem::path_preferences_get() + "recent.txt"; std::filesystem::path Manager::autosave_directory_get() { return filesystem::path_preferences_get() + "autosave"; }
}
std::filesystem::path Manager::autosave_path_get()
{
return filesystem::path_preferences_get() + "autosave.txt";
}
std::filesystem::path Manager::autosave_directory_get()
{
return filesystem::path_preferences_get() + "autosave";
}
Manager::Manager() Manager::Manager()
{ {
@@ -35,10 +24,7 @@ namespace anm2ed
autosave_files_load(); autosave_files_load();
} }
Document* Manager::get(int index) Document* Manager::get(int index) { return vector::find(documents, index > -1 ? index : selected); }
{
return vector::find(documents, index > -1 ? index : selected);
}
void Manager::open(const std::string& path, bool isNew, bool isRecent) void Manager::open(const std::string& path, bool isNew, bool isRecent)
{ {
@@ -68,10 +54,7 @@ namespace anm2ed
toasts.info(std::format("Opened document: {}", path)); toasts.info(std::format("Opened document: {}", path));
} }
void Manager::new_(const std::string& path) void Manager::new_(const std::string& path) { open(path, true); }
{
open(path, true);
}
void Manager::save(int index, const std::string& path) void Manager::save(int index, const std::string& path)
{ {
@@ -83,10 +66,7 @@ namespace anm2ed
} }
} }
void Manager::save(const std::string& path) void Manager::save(const std::string& path) { save(selected, path); }
{
save(selected, path);
}
void Manager::autosave(Document& document) void Manager::autosave(Document& document)
{ {
@@ -155,15 +135,9 @@ namespace anm2ed
} }
} }
void Manager::layer_properties_trigger() void Manager::layer_properties_trigger() { layerPropertiesPopup.trigger(); }
{
layerPropertiesPopup.trigger();
}
void Manager::layer_properties_end() void Manager::layer_properties_end() { layerPropertiesPopup.end(); }
{
layerPropertiesPopup.end();
}
void Manager::layer_properties_close() void Manager::layer_properties_close()
{ {
@@ -186,15 +160,9 @@ namespace anm2ed
} }
} }
void Manager::null_properties_trigger() void Manager::null_properties_trigger() { nullPropertiesPopup.trigger(); }
{
nullPropertiesPopup.trigger();
}
void Manager::null_properties_end() void Manager::null_properties_end() { nullPropertiesPopup.end(); }
{
nullPropertiesPopup.end();
}
void Manager::null_properties_close() void Manager::null_properties_close()
{ {
@@ -313,8 +281,5 @@ namespace anm2ed
autosave_files_write(); autosave_files_write();
} }
Manager::~Manager() Manager::~Manager() { autosave_files_clear(); }
{
autosave_files_clear();
}
} }

View File

@@ -1,7 +1,10 @@
#include "render.h" #include "render.h"
#include <chrono>
#include <cstring> #include <cstring>
#include <filesystem>
#include <format> #include <format>
#include <fstream>
#ifdef _WIN32 #ifdef _WIN32
#include "util.h" #include "util.h"
@@ -9,7 +12,7 @@
#define PCLOSE _pclose #define PCLOSE _pclose
#define PWRITE_MODE "wb" #define PWRITE_MODE "wb"
#define PREAD_MODE "r" #define PREAD_MODE "r"
#else #elif __unix__
#define POPEN popen #define POPEN popen
#define PCLOSE pclose #define PCLOSE pclose
#define PWRITE_MODE "w" #define PWRITE_MODE "w"
@@ -25,44 +28,87 @@ namespace anm2ed
{ {
constexpr auto FFMPEG_POPEN_ERROR = "popen() (for FFmpeg) failed!\n{}"; constexpr auto FFMPEG_POPEN_ERROR = "popen() (for FFmpeg) failed!\n{}";
constexpr auto 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}\"";
constexpr auto 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}\"";
constexpr auto* 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 animation_render(const std::string& ffmpegPath, const std::string& path, std::vector<Texture>& frames, bool animation_render(const std::string& ffmpegPath, const std::string& path, std::vector<Texture>& frames,
render::Type type, ivec2 size, int fps) AudioStream& audioStream, render::Type type, ivec2 size, int fps)
{ {
if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || path.empty()) return false; if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || path.empty()) return false;
std::filesystem::path audioPath{};
std::string audioInputArguments{};
std::string audioOutputArguments{"-an"};
std::string command{}; std::string command{};
auto remove_audio_file = [&]()
{
if (!audioPath.empty())
{
std::error_code ec;
std::filesystem::remove(audioPath, ec);
}
};
if (type != render::GIF && !audioStream.stream.empty() && audioStream.spec.freq > 0 &&
audioStream.spec.channels > 0)
{
audioPath = std::filesystem::temp_directory_path() / std::format("{}.f32", path);
std::ofstream audioFile(audioPath, std::ios::binary);
if (audioFile)
{
auto data = (const char*)audioStream.stream.data();
auto byteCount = audioStream.stream.size() * sizeof(float);
audioFile.write(data, byteCount);
audioFile.close();
audioInputArguments = std::format("-f f32le -ar {0} -ac {1} -i \"{2}\"", audioStream.spec.freq,
audioStream.spec.channels, audioPath.string());
switch (type)
{
case render::WEBM:
audioOutputArguments = "-c:a libopus -b:a 160k -shortest";
break;
case render::MP4:
audioOutputArguments = "-c:a aac -b:a 192k -shortest";
break;
default:
break;
}
}
else
{
logger.warning("Failed to open temporary audio file; exporting video without audio.");
remove_audio_file();
}
}
command = std::format("\"{0}\" -y -f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0", ffmpegPath, size.x,
size.y, fps);
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
switch (type) switch (type)
{ {
case render::GIF: case render::GIF:
command = std::format(GIF_FORMAT, ffmpegPath, size.x, size.y, fps, path); command +=
" -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full[p];[s1][p]paletteuse=dither=floyd_steinberg\""
" -loop 0";
command += std::format(" \"{}\"", path);
break; break;
case render::WEBM: case render::WEBM:
command = std::format(WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, path); command += " -c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 -auto-alt-ref 0";
if (!audioOutputArguments.empty()) command += " " + audioOutputArguments;
command += std::format(" \"{}\"", path);
break; break;
case render::MP4: case render::MP4:
command = std::format(MP4_FORMAT, ffmpegPath, size.x, size.y, fps, path); command += " -vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" -c:v libx265 -crf 20 -preset slow"
" -tag:v hvc1 -movflags +faststart";
if (!audioOutputArguments.empty()) command += " " + audioOutputArguments;
command += std::format(" \"{}\"", path);
break; break;
default: default:
break; return false;
} }
#if _WIN32 #if _WIN32
@@ -75,6 +121,7 @@ namespace anm2ed
if (!fp) if (!fp)
{ {
remove_audio_file();
logger.error(std::format(FFMPEG_POPEN_ERROR, strerror(errno))); logger.error(std::format(FFMPEG_POPEN_ERROR, strerror(errno)));
return false; return false;
} }
@@ -85,12 +132,14 @@ namespace anm2ed
if (fwrite(frame.pixels.data(), 1, frameSize, fp) != frameSize) if (fwrite(frame.pixels.data(), 1, frameSize, fp) != frameSize)
{ {
remove_audio_file();
PCLOSE(fp); PCLOSE(fp);
return false; return false;
} }
} }
auto code = PCLOSE(fp); auto code = PCLOSE(fp);
remove_audio_file();
return (code == 0); return (code == 0);
} }
} }

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "audio_stream.h"
#include "texture.h" #include "texture.h"
namespace anm2ed::render namespace anm2ed::render
@@ -33,6 +34,6 @@ namespace anm2ed::render
namespace anm2ed namespace anm2ed
{ {
bool animation_render(const std::string&, const std::string&, std::vector<resource::Texture>&, render::Type, bool animation_render(const std::string&, const std::string&, std::vector<resource::Texture>&, AudioStream&,
glm::ivec2, int); render::Type, glm::ivec2, int);
} }

View File

@@ -0,0 +1,26 @@
#include "audio_stream.h"
namespace anm2ed
{
void AudioStream::callback(void* userData, MIX_Mixer* mixer, const SDL_AudioSpec* spec, float* pcm, int samples)
{
auto self = (AudioStream*)userData;
self->stream.insert(self->stream.end(), pcm, pcm + samples);
}
AudioStream::AudioStream(MIX_Mixer* mixer)
{
MIX_GetMixerFormat(mixer, &spec);
}
void AudioStream::capture_begin(MIX_Mixer* mixer)
{
MIX_SetPostMixCallback(mixer, callback, this);
}
void AudioStream::capture_end(MIX_Mixer* mixer)
{
MIX_SetPostMixCallback(mixer, nullptr, this);
stream.clear();
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <SDL3_mixer/SDL_mixer.h>
#include <vector>
namespace anm2ed
{
class AudioStream
{
static void callback(void*, MIX_Mixer*, const SDL_AudioSpec*, float*, int);
public:
std::vector<float> stream{};
SDL_AudioSpec spec{};
AudioStream(MIX_Mixer*);
void capture_begin(MIX_Mixer*);
void capture_end(MIX_Mixer*);
};
}

View File

@@ -153,7 +153,7 @@ namespace anm2ed::resource::icon
<svg viewBox="0 0 24 48" fill="#FFF" xmlns="http://www.w3.org/2000/svg"> <path d="M4 0H20V38L12 48L4 38V0Z"/> </svg> <svg viewBox="0 0 24 48" fill="#FFF" xmlns="http://www.w3.org/2000/svg"> <path d="M4 0H20V38L12 48L4 38V0Z"/> </svg>
)"; )";
#define LIST \ #define SVG_LIST \
X(NONE, NONE_DATA, SIZE_SMALL) \ X(NONE, NONE_DATA, SIZE_SMALL) \
X(FILE, FILE_DATA, SIZE_NORMAL) \ X(FILE, FILE_DATA, SIZE_NORMAL) \
X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \ X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \
@@ -195,7 +195,7 @@ namespace anm2ed::resource::icon
enum Type enum Type
{ {
#define X(name, data, size) name, #define X(name, data, size) name,
LIST SVG_LIST
#undef X #undef X
COUNT COUNT
}; };
@@ -209,7 +209,21 @@ namespace anm2ed::resource::icon
const Info ICONS[COUNT] = { const Info ICONS[COUNT] = {
#define X(name, data, size) {data, std::strlen(data) - 1, size}, #define X(name, data, size) {data, std::strlen(data) - 1, size},
LIST SVG_LIST
#undef X #undef X
}; };
constexpr uint8_t PROGRAM[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x40, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x60, 0xb9, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70,
0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00,
0x8b, 0x49, 0x44, 0x41, 0x54, 0x68, 0xde, 0xed, 0xd7, 0x3d, 0x0e, 0x80, 0x20, 0x0c, 0x86, 0x61, 0x7b, 0x53, 0x3c,
0x99, 0xbd, 0x69, 0x1d, 0x8c, 0x83, 0x4d, 0xd0, 0x44, 0x29, 0x7f, 0xbe, 0x5d, 0x08, 0x30, 0xf4, 0x19, 0x80, 0x2f,
0xc8, 0xd2, 0xb8, 0x04, 0xc0, 0xb4, 0x80, 0x64, 0xc7, 0xb8, 0xf9, 0x86, 0x02, 0x60, 0x36, 0x80, 0xd9, 0x75, 0xbe,
0xba, 0x7d, 0x00, 0xf3, 0x00, 0x72, 0x8d, 0x7c, 0x83, 0x73, 0x5d, 0xa5, 0xf0, 0x43, 0x04, 0xa0, 0x1a, 0x20, 0xaa,
0x11, 0x80, 0xf1, 0x01, 0x1a, 0x14, 0x5b, 0x00, 0xfa, 0x03, 0x24, 0xbb, 0x0f, 0x93, 0xa7, 0xb0, 0xf9, 0x1c, 0x46,
0x00, 0x00, 0x00, 0xe8, 0x06, 0xa0, 0xa5, 0x3f, 0x2a, 0x99, 0x07, 0x0d, 0x40, 0xbf, 0x80, 0xa8, 0x02, 0x30, 0x0e,
0x40, 0xdf, 0x1e, 0xb2, 0x56, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x07, 0x90, 0x86, 0xff, 0x05, 0xd4,
0x2e, 0x00, 0x00, 0x00, 0x34, 0x07, 0xec, 0x94, 0x51, 0xac, 0x41, 0x55, 0x6e, 0xe4, 0x26, 0x00, 0x00, 0x00, 0x00,
0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
} }

View File

@@ -28,15 +28,9 @@ using namespace glm;
namespace anm2ed::resource namespace anm2ed::resource
{ {
bool Texture::is_valid() bool Texture::is_valid() { return id != 0; }
{
return id != 0;
}
size_t Texture::pixel_size_get() size_t Texture::pixel_size_get() { return size.x * size.y * CHANNELS; }
{
return size.x * size.y * CHANNELS;
}
void Texture::upload(const uint8_t* data) void Texture::upload(const uint8_t* data)
{ {
@@ -66,15 +60,9 @@ namespace anm2ed::resource
if (is_valid()) glDeleteTextures(1, &id); if (is_valid()) glDeleteTextures(1, &id);
} }
Texture::Texture(const Texture& other) Texture::Texture(const Texture& other) { *this = other; }
{
*this = other;
}
Texture::Texture(Texture&& other) Texture::Texture(Texture&& other) { *this = std::move(other); }
{
*this = std::move(other);
}
Texture& Texture::operator=(const Texture& other) // Copy Texture& Texture::operator=(const Texture& other) // Copy
{ {

View File

@@ -26,8 +26,8 @@ namespace anm2ed::resource
size_t pixel_size_get(); size_t pixel_size_get();
void upload(); void upload();
void upload(const uint8_t*); void upload(const uint8_t*);
Texture();
Texture();
~Texture(); ~Texture();
Texture(const Texture&); Texture(const Texture&);
Texture(Texture&&); Texture(Texture&&);

View File

@@ -10,97 +10,97 @@ namespace anm2ed
{ {
constexpr auto IMGUI_DEFAULT = R"( constexpr auto IMGUI_DEFAULT = R"(
# Dear ImGui # Dear ImGui
[Window][## Window] [Window][##DockSpace]
Pos=0,32 Pos=0,54
Size=1600,868 Size=1918,1010
Collapsed=0 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] [Window][Animation Preview]
Pos=48,40 Pos=60,62
Size=907,516 Size=983,691
Collapsed=0
DockId=0x00000003,1
[Window][Animations]
Pos=1451,494
Size=459,259
Collapsed=0 Collapsed=0
DockId=0x0000000C,0 DockId=0x0000000C,0
[Window][Spritesheet Editor] [Window][Events]
Pos=48,40 Pos=1045,463
Size=907,516 Size=404,290
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 Collapsed=0
DockId=0x00000008,0 DockId=0x00000008,0
[Window][Nulls] [Window][Frame Properties]
Pos=957,264 Pos=1045,62
Size=330,292 Size=404,399
Collapsed=0
DockId=0x00000007,0
[Window][Layers]
Pos=1045,463
Size=404,290
Collapsed=0 Collapsed=0
DockId=0x00000008,1 DockId=0x00000008,1
[Window][Nulls]
Pos=1045,463
Size=404,290
Collapsed=0
DockId=0x00000008,2
[Window][Onionskin]
Pos=8,755
Size=1902,301
Collapsed=0
DockId=0x00000006,1
[Window][Spritesheet Editor]
Pos=60,62
Size=983,691
Collapsed=0
DockId=0x00000003,0
[Window][Spritesheets]
Pos=1451,62
Size=459,430
Collapsed=0
DockId=0x0000000B,0
[Window][Tools]
Pos=8,62
Size=50,691
Collapsed=0
DockId=0x00000001,0
[Window][Timeline]
Pos=8,755
Size=1902,301
Collapsed=0
DockId=0x00000006,0
[Window][Sounds]
Pos=1045,463
Size=404,290
Collapsed=0
DockId=0x00000008,3
[Docking][Data] [Docking][Data]
DockSpace ID=0xFC02A410 Window=0x0E46F4F7 Pos=8,40 Size=1584,852 Split=Y DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1902,994 Split=Y Selected=0x4EFD0020
DockNode ID=0x00000003 Parent=0xFC02A410 SizeRef=1902,680 Split=X DockNode ID=0x00000005 Parent=0x123F8F08 SizeRef=1910,691 Split=X
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=1017,1016 Split=X Selected=0x024430EF DockNode ID=0x00000001 Parent=0x00000005 SizeRef=50,994 Selected=0x18A5FDB9
DockNode ID=0x00000005 Parent=0x00000001 SizeRef=1264,654 Split=X Selected=0x024430EF DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1850,994 Split=X Selected=0x4EFD0020
DockNode ID=0x0000000B Parent=0x00000005 SizeRef=38,654 Selected=0x18A5FDB9 DockNode ID=0x00000003 Parent=0x00000002 SizeRef=983,994 Selected=0x024430EF
DockNode ID=0x0000000C Parent=0x00000005 SizeRef=1224,654 CentralNode=1 Selected=0x024430EF DockNode ID=0x00000004 Parent=0x00000002 SizeRef=865,994 Split=X Selected=0x4EFD0020
DockNode ID=0x00000006 Parent=0x00000001 SizeRef=330,654 Split=Y Selected=0x754E368F DockNode ID=0x00000009 Parent=0x00000004 SizeRef=404,497 Split=Y Selected=0xCD8384B1
DockNode ID=0x00000007 Parent=0x00000006 SizeRef=631,293 Selected=0x754E368F DockNode ID=0x00000007 Parent=0x00000009 SizeRef=181,399 Selected=0x754E368F
DockNode ID=0x00000008 Parent=0x00000006 SizeRef=631,385 Selected=0xCD8384B1 DockNode ID=0x00000008 Parent=0x00000009 SizeRef=181,290 Selected=0x8A65D963
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=303,1016 Split=Y Selected=0x4EFD0020 DockNode ID=0x0000000A Parent=0x00000004 SizeRef=459,497 Split=Y Selected=0x4EFD0020
DockNode ID=0x00000009 Parent=0x00000002 SizeRef=634,349 Selected=0x4EFD0020 DockNode ID=0x0000000B Parent=0x0000000A SizeRef=710,430 CentralNode=1 Selected=0x4EFD0020
DockNode ID=0x0000000A Parent=0x00000002 SizeRef=634,329 Selected=0xC1986EE2 DockNode ID=0x0000000C Parent=0x0000000A SizeRef=710,259 Selected=0xC1986EE2
DockNode ID=0x00000004 Parent=0xFC02A410 SizeRef=1902,334 Selected=0x4F89F0DC DockNode ID=0x00000006 Parent=0x123F8F08 SizeRef=1910,301 Selected=0x4F89F0DC
)"; )";
Settings::Settings(const std::string& path) Settings::Settings(const std::string& path)

View File

@@ -153,8 +153,8 @@ namespace anm2ed
/* Symbol / Name / String / Type / Default */ \ /* Symbol / Name / String / Type / Default */ \
X(SHORTCUT_CENTER_VIEW, shortcutCenterView, "Center View", STRING, "Home") \ X(SHORTCUT_CENTER_VIEW, shortcutCenterView, "Center View", STRING, "Home") \
X(SHORTCUT_FIT, shortcutFit, "Fit", STRING, "F") \ X(SHORTCUT_FIT, shortcutFit, "Fit", STRING, "F") \
X(SHORTCUT_ZOOM_IN, shortcutZoomIn, "Zoom In", STRING, "Ctrl++") \ X(SHORTCUT_ZOOM_IN, shortcutZoomIn, "Zoom In", STRING, "Ctrl+Equal") \
X(SHORTCUT_ZOOM_OUT, shortcutZoomOut, "Zoom Out", STRING, "Ctrl+-") \ X(SHORTCUT_ZOOM_OUT, shortcutZoomOut, "Zoom Out", STRING, "Ctrl+Minus") \
X(SHORTCUT_PLAY_PAUSE, shortcutPlayPause, "Play/Pause", STRING, "Space") \ X(SHORTCUT_PLAY_PAUSE, shortcutPlayPause, "Play/Pause", STRING, "Space") \
X(SHORTCUT_ONIONSKIN, shortcutOnionskin, "Onionskin", STRING, "O") \ X(SHORTCUT_ONIONSKIN, shortcutOnionskin, "Onionskin", STRING, "O") \
X(SHORTCUT_NEW, shortcutNew, "New", STRING, "Ctrl+N") \ X(SHORTCUT_NEW, shortcutNew, "New", STRING, "Ctrl+N") \

View File

@@ -4,10 +4,7 @@ using namespace anm2ed::snapshots;
namespace anm2ed namespace anm2ed
{ {
bool SnapshotStack::is_empty() bool SnapshotStack::is_empty() { return top == 0; }
{
return top == 0;
}
void SnapshotStack::push(const Snapshot& snapshot) void SnapshotStack::push(const Snapshot& snapshot)
{ {
@@ -26,10 +23,7 @@ namespace anm2ed
return &snapshots[--top]; return &snapshots[--top];
} }
void SnapshotStack::clear() void SnapshotStack::clear() { top = 0; }
{
top = 0;
}
void Snapshots::push(const Snapshot& snapshot) void Snapshots::push(const Snapshot& snapshot)
{ {

View File

@@ -25,6 +25,8 @@ namespace anm2ed
Storage null{}; Storage null{};
Storage sound{}; Storage sound{};
Storage spritesheet{}; Storage spritesheet{};
Storage items{};
std::map<int, Storage> frames{};
std::string message = snapshots::ACTION; std::string message = snapshots::ACTION;
}; };

161
src/socket.cpp Normal file
View File

@@ -0,0 +1,161 @@
#include "socket.h"
namespace anm2ed
{
#ifdef _WIN32
namespace
{
struct WSAInitializer
{
WSAInitializer()
{
WSADATA data{};
WSAStartup(MAKEWORD(2, 2), &data);
}
~WSAInitializer() { WSACleanup(); }
};
WSAInitializer initializer{};
}
#endif
Socket::Socket() : handle(SOCKET_INVALID), role(CLIENT) {}
Socket::Socket(Socket&& other) noexcept : handle(other.handle), role(other.role) { other.handle = SOCKET_INVALID; }
Socket& Socket::operator=(Socket&& other) noexcept
{
if (this != &other)
{
close();
handle = other.handle;
role = other.role;
other.handle = SOCKET_INVALID;
}
return *this;
}
Socket::~Socket() { close(); }
bool Socket::open(SocketRole newRole)
{
close();
role = newRole;
handle = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (!is_valid()) return false;
if (role == SERVER)
{
#ifdef _WIN32
BOOL opt = TRUE;
#else
int opt = 1;
#endif
::setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&opt), sizeof(opt));
}
return true;
}
bool Socket::bind(const SocketAddress& address)
{
if (!is_valid()) return false;
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(address.port);
if (address.host.empty())
addr.sin_addr.s_addr = htonl(INADDR_ANY);
else
{
if (::inet_pton(AF_INET, address.host.c_str(), &addr.sin_addr) <= 0) return false;
}
return ::bind(handle, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0;
}
bool Socket::listen()
{
if (!is_valid()) return false;
return ::listen(handle, SOMAXCONN) == 0;
}
Socket Socket::accept()
{
Socket client{};
if (!is_valid()) return client;
auto accepted = ::accept(handle, nullptr, nullptr);
if (accepted == SOCKET_INVALID) return client;
client.close();
client.handle = accepted;
client.role = CLIENT;
return client;
}
bool Socket::connect(const SocketAddress& address)
{
if (!is_valid()) return false;
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(address.port);
if (::inet_pton(AF_INET, address.host.c_str(), &addr.sin_addr) <= 0) return false;
return ::connect(handle, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0;
}
bool Socket::send(const void* data, size_t size)
{
if (!is_valid() || !data || size == 0) return false;
auto bytes = reinterpret_cast<const char*>(data);
size_t totalSent = 0;
while (totalSent < size)
{
auto sent = ::send(handle, bytes + totalSent, static_cast<int>(size - totalSent), 0);
if (sent <= 0) return false;
totalSent += static_cast<size_t>(sent);
}
return true;
}
bool Socket::receive(void* buffer, size_t size)
{
if (!is_valid() || !buffer || size == 0) return false;
auto* bytes = reinterpret_cast<char*>(buffer);
size_t totalReceived = 0;
while (totalReceived < size)
{
auto received = ::recv(handle, bytes + totalReceived, static_cast<int>(size - totalReceived), 0);
if (received <= 0) return false;
totalReceived += static_cast<size_t>(received);
}
return true;
}
void Socket::close()
{
if (!is_valid()) return;
#ifdef _WIN32
::closesocket(handle);
#else
::close(handle);
#endif
handle = SOCKET_INVALID;
}
bool Socket::is_valid() const { return handle != SOCKET_INVALID; }
}

61
src/socket.h Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
#include <cstddef>
#include <string>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using socket_handle = SOCKET;
constexpr socket_handle SOCKET_INVALID = INVALID_SOCKET;
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using socket_handle = int;
constexpr socket_handle SOCKET_INVALID = -1;
#endif
namespace anm2ed
{
enum SocketRole
{
SERVER,
CLIENT
};
struct SocketAddress
{
std::string host{};
unsigned short port{};
};
class Socket
{
private:
socket_handle handle;
SocketRole role{};
public:
Socket();
~Socket();
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&& other) noexcept;
Socket& operator=(Socket&& other) noexcept;
bool open(SocketRole role);
bool bind(const SocketAddress&);
bool listen();
Socket accept();
bool connect(const SocketAddress&);
bool send(const void*, size_t);
bool receive(void*, size_t);
void close();
bool is_valid() const;
};
}

View File

@@ -52,7 +52,10 @@ namespace anm2ed
{ {
auto droppedFile = event.drop.data; auto droppedFile = event.drop.data;
if (filesystem::path_is_extension(droppedFile, "anm2")) if (filesystem::path_is_extension(droppedFile, "anm2"))
{
manager.open(std::string(droppedFile)); manager.open(std::string(droppedFile));
SDL_FlashWindow(window, SDL_FLASH_UNTIL_FOCUSED);
}
else if (filesystem::path_is_extension(droppedFile, "png")) else if (filesystem::path_is_extension(droppedFile, "png"))
{ {
if (auto document = manager.get()) if (auto document = manager.get())

View File

@@ -8,14 +8,37 @@
namespace anm2ed::util::filesystem namespace anm2ed::util::filesystem
{ {
std::string path_preferences_get() std::string path_pref_get(const char* org, const char* app)
{ {
char* preferencesPath = SDL_GetPrefPath("", "anm2ed"); auto path = SDL_GetPrefPath(org, app);
std::string preferencesPathString = preferencesPath; std::string string = path;
SDL_free(preferencesPath); SDL_free(path);
return preferencesPathString; return string;
} }
std::string path_preferences_get() { return path_pref_get(nullptr, "anm2ed"); }
std::string path_base_get() { return std::string(SDL_GetBasePath()); }
std::string path_executable_get() { return std::filesystem::path(path_base_get()) / "anm2ed"; }
#ifdef __unix__
std::string path_application_get()
{
return std::filesystem::path(path_pref_get(nullptr, "applications")) / "anm2ed.desktop";
}
std::string path_mime_get()
{
return std::filesystem::path(path_pref_get(nullptr, "mime/application")) / "x-anm2+xml.xml";
}
std::string path_icon_get() { return std::filesystem::path(path_preferences_get()) / "anm2ed.png"; }
std::string path_icon_file_get()
{
return std::filesystem::path(path_preferences_get()) / "application-x-anm2+xml.png";
}
#endif
bool path_is_exist(const std::string& path) bool path_is_exist(const std::string& path)
{ {
std::error_code errorCode; std::error_code errorCode;
@@ -49,8 +72,5 @@ namespace anm2ed::util::filesystem
std::filesystem::current_path(path); std::filesystem::current_path(path);
} }
WorkingDirectory::~WorkingDirectory() WorkingDirectory::~WorkingDirectory() { std::filesystem::current_path(previous); }
{
std::filesystem::current_path(previous);
}
} }

View File

@@ -5,9 +5,21 @@
namespace anm2ed::util::filesystem namespace anm2ed::util::filesystem
{ {
#ifdef __unix__
std::string path_application_get();
std::string path_mime_get();
std::string path_icon_get();
std::string path_icon_file_get();
#endif
std::string path_pref_get();
std::string path_preferences_get(); std::string path_preferences_get();
std::string path_base_get();
std::string path_executable_get();
bool path_is_exist(const std::string&); bool path_is_exist(const std::string&);
bool path_is_extension(const std::string&, const std::string&); bool path_is_extension(const std::string&, const std::string&);
std::filesystem::path path_lower_case_backslash_handle(std::filesystem::path&); std::filesystem::path path_lower_case_backslash_handle(std::filesystem::path&);
class WorkingDirectory class WorkingDirectory