Anm2 merge and adjustments to render animation

This commit is contained in:
2025-11-13 01:19:25 -05:00
parent e4cb0056a0
commit bb6b68311b
5 changed files with 351 additions and 266 deletions

View File

@@ -5,7 +5,9 @@
#include <cfloat>
#include <cmath>
#include <filesystem>
#include <format>
#include <ranges>
#include <tuple>
#include <vector>
#include <imgui/imgui.h>
@@ -16,7 +18,6 @@
#include "types.h"
#include "icon.h"
#include "toast.h"
using namespace anm2ed::resource;
using namespace anm2ed::types;
@@ -26,6 +27,107 @@ using namespace glm;
namespace anm2ed::imgui
{
static constexpr auto ANM2ED_LABEL = "Anm2Ed";
static constexpr auto VERSION_LABEL = "Version 2.0";
static constexpr auto CREDIT_DELAY = 1.0f;
static constexpr auto CREDIT_SCROLL_SPEED = 25.0f;
struct Credit
{
const char* string{};
font::Type font{font::REGULAR};
};
struct ScrollingCredit
{
int index{};
float offset{};
};
struct CreditsState
{
std::vector<ScrollingCredit> active{};
float spawnTimer{1.0f};
int nextIndex{};
};
static constexpr Credit CREDITS[] = {
{"Anm2Ed", font::BOLD},
{"License: GPLv3"},
{""},
{"Designer", font::BOLD},
{"Shweet"},
{""},
{"Additional Help", font::BOLD},
{"im-tem"},
{""},
{"Based on the work of:", font::BOLD},
{"Adrian Gavrilita"},
{"Simon Parzer"},
{"Matt Kapuszczak"},
{""},
{"XM Music", font::BOLD},
{"Drozerix"},
{"\"Keygen Wraith\""},
{"https://modarchive.org/module.php?207854"},
{"License: CC0"},
{""},
{"Libraries", font::BOLD},
{"Dear ImGui"},
{"https://github.com/ocornut/imgui"},
{"License: MIT"},
{""},
{"SDL"},
{"https://github.com/libsdl-org/SDL"},
{"License: zlib"},
{""},
{"SDL_mixer"},
{"https://github.com/libsdl-org/SDL_mixer"},
{"License: zlib"},
{""},
{"tinyxml2"},
{"https://github.com/leethomason/tinyxml2"},
{"License: zlib"},
{""},
{"glm"},
{"https://github.com/g-truc/glm"},
{"License: MIT"},
{""},
{"lunasvg"},
{"https://github.com/sammycage/lunasvg"},
{"License: MIT"},
{""},
{"Icons", font::BOLD},
{"Remix Icons"},
{"remixicon.com"},
{"License: Apache"},
{""},
{"Font", font::BOLD},
{"Noto Sans"},
{"https://fonts.google.com/noto/specimen/Noto+Sans"},
{"License: OFL"},
{""},
{"Special Thanks", font::BOLD},
{"Edmund McMillen"},
{"Florian Himsl"},
{"Tyrone Rodriguez"},
{"The-Vinh Truong (_kilburn)"},
{"Everyone who waited patiently for this to be finished"},
{"Everyone else who has worked on The Binding of Isaac!"},
{""},
{""},
{""},
{""},
{""},
{"enjoy the jams :)"},
{""},
{""},
{""},
{""},
{""},
};
static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(Credit));
Taskbar::Taskbar() : generate(vec2()) {}
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
@@ -43,9 +145,10 @@ namespace anm2ed::imgui
if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.file_save(dialog::ANM2_NEW);
if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.file_open(dialog::ANM2_OPEN);
if (ImGui::BeginMenu("Open Recent", !manager.recentFiles.empty()))
auto recentFiles = manager.recent_files_ordered();
if (ImGui::BeginMenu("Open Recent", !recentFiles.empty()))
{
for (auto [i, file] : std::views::enumerate(manager.recentFiles))
for (auto [i, file] : std::views::enumerate(recentFiles))
{
auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string());
@@ -54,7 +157,7 @@ namespace anm2ed::imgui
ImGui::PopID();
}
if (!manager.recentFiles.empty())
if (!recentFiles.empty())
if (ImGui::MenuItem("Clear List")) manager.recent_files_clear();
ImGui::EndMenu();
@@ -407,7 +510,6 @@ namespace anm2ed::imgui
if (ImGui::BeginPopupModal(renderPopup.label, &renderPopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto& playback = document->playback;
auto& ffmpegPath = settings.renderFFmpegPath;
auto& path = settings.renderPath;
auto& format = settings.renderFormat;
@@ -416,27 +518,88 @@ namespace anm2ed::imgui
auto& type = settings.renderType;
auto& start = manager.recordingStart;
auto& end = manager.recordingEnd;
auto& rows = settings.renderRows;
auto& columns = settings.renderColumns;
auto& isRange = manager.isRecordingRange;
auto widgetSize = widget_size_with_row_get(2);
auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET
: type == render::GIF ? dialog::GIF_PATH_SET
: type == render::WEBM ? dialog::WEBM_PATH_SET
: dialog::NONE;
auto& frames = document->frames.selection;
int length = std::max(1, end - start + 1);
auto range_set = [&]()
{
if (!frames.empty())
{
if (auto item = document->item_get())
{
int duration{};
for (auto [i, frame] : std::views::enumerate(item->frames))
{
if (i == *frames.begin())
start = duration;
else if (i == *frames.rbegin())
{
end = duration;
break;
}
duration += frame.duration;
}
}
}
else if (!isRange)
{
start = 0;
end = animation->frameNum - 1;
}
length = std::max(1, end - start + 1);
};
auto rows_columns_set = [&]()
{
auto framesNeeded = std::max(1, length);
int bestRows = 1;
int bestColumns = framesNeeded;
auto bestScore = std::make_tuple(bestColumns - bestRows, bestColumns * bestRows - framesNeeded, -bestColumns);
for (int candidateRows = 1; candidateRows <= framesNeeded; ++candidateRows)
{
int candidateColumns = (framesNeeded + candidateRows - 1) / candidateRows;
if (candidateColumns < candidateRows) break;
auto candidateScore = std::make_tuple(candidateColumns - candidateRows,
candidateColumns * candidateRows - framesNeeded, -candidateColumns);
if (candidateScore < bestScore)
{
bestScore = candidateScore;
bestRows = candidateRows;
bestColumns = candidateColumns;
}
}
rows = bestRows;
columns = bestColumns;
};
auto replace_extension = [&]()
{ path = std::filesystem::path(path).replace_extension(render::EXTENSIONS[type]).string(); };
auto range_to_length = [&]()
{
start = 0;
end = animation->frameNum;
};
if (renderPopup.isJustOpened)
auto render_set = [&]()
{
replace_extension();
if (!isRange) range_to_length();
}
range_set();
rows_columns_set();
};
auto widgetSize = widget_size_with_row_get(2);
auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET
: type == render::SPRITESHEET ? dialog::PNG_PATH_SET
: type == render::GIF ? dialog::GIF_PATH_SET
: type == render::WEBM ? dialog::WEBM_PATH_SET
: dialog::NONE;
if (renderPopup.isJustOpened) render_set();
if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, icon_size_get()))
dialog.file_open(dialog::FFMPEG_PATH_SET);
@@ -458,41 +621,64 @@ namespace anm2ed::imgui
ImGui::SetItemTooltip("Set the output path or directory for the animation.");
dialog.set_string_to_selected_path(path, dialogType);
if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) replace_extension();
if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) render_set();
ImGui::SetItemTooltip("Set the type of the output.");
if (type == render::PNGS || type == render::SPRITESHEET) ImGui::Separator();
if (type == render::PNGS)
{
ImGui::Separator();
input_text_string("Format", &format);
if (input_text_string("Format", &format)) format = std::filesystem::path(format).replace_extension(".png");
ImGui::SetItemTooltip(
"For outputted images, each image will use this format.\n{} represents the index of each image.");
}
else if (type == render::SPRITESHEET)
{
input_int_range("Rows", rows, 1, length);
ImGui::SetItemTooltip("Set how many rows the spritesheet will have.");
input_int_range("Columns", columns, 1, length);
ImGui::SetItemTooltip("Set how many columns the spritesheet will have.");
if (ImGui::Button("Set to Recommended")) rows_columns_set();
ImGui::SetItemTooltip("Use a recommended value for rows/columns.");
}
ImGui::Separator();
if (ImGui::Checkbox("Custom Range", &isRange))
if (!isRange) range_to_length();
ImGui::SetItemTooltip("Toggle using a custom range for the animation.");
{
range_set();
ImGui::SetItemTooltip("Toggle using a custom range for the animation.");
}
if (isRange)
ImGui::SameLine();
ImGui::BeginDisabled(frames.empty());
if (ImGui::Button("To Selected Frames")) range_set();
ImGui::SetItemTooltip("If frames are selected, use that range for the rendered animation.");
ImGui::EndDisabled();
ImGui::BeginDisabled(!isRange);
{
input_int_range("Start", start, 0, animation->frameNum - 1);
ImGui::SetItemTooltip("Set the starting time of the animation.");
ImGui::SetItemTooltip("Set the starting time of the animation.");
input_int_range("End", end, start + 1, animation->frameNum);
ImGui::SetItemTooltip("Set the ending time of the animation.");
ImGui::SetItemTooltip("Set the ending time of the animation.");
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::Checkbox("Raw", &isRaw);
ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds.");
if (isRaw)
ImGui::BeginDisabled(!isRaw);
{
input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx");
ImGui::SetItemTooltip("Set the output scale of the animation.");
}
ImGui::EndDisabled();
ImGui::Separator();
@@ -504,78 +690,11 @@ namespace anm2ed::imgui
if (ImGui::Button("Render", widgetSize))
{
bool canStart = true;
auto warn_and_close = [&](const std::string& message)
{
toasts.warning(message);
renderPopup.close();
canStart = false;
};
auto ffmpegPathValid = [&]() -> bool
{
if (ffmpegPath.empty()) return false;
std::error_code ec{};
std::filesystem::path ffmpeg(ffmpegPath);
if (!std::filesystem::exists(ffmpeg, ec) || !std::filesystem::is_regular_file(ffmpeg, ec)) return false;
#ifdef _WIN32
auto ext = ffmpeg.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return (char)std::tolower(c); });
if (ext != ".exe") return false;
return true;
#else
auto permMask = std::filesystem::status(ffmpeg, ec).permissions();
using std::filesystem::perms;
if (permMask == perms::unknown) return true;
auto has_exec = [&](perms p)
{
return (perms::none != (p & perms::owner_exec)) || (perms::none != (p & perms::group_exec)) ||
(perms::none != (p & perms::others_exec));
};
return has_exec(permMask);
#endif
};
if (!ffmpegPathValid()) warn_and_close("Invalid FFmpeg executable. Please set a valid FFmpeg path.");
if (canStart)
{
std::error_code ec{};
if (type == render::PNGS)
{
if (path.empty())
warn_and_close("Select an output directory for PNG exports.");
else
{
std::filesystem::path directory(path);
if (!std::filesystem::exists(directory, ec) || !std::filesystem::is_directory(directory, ec))
warn_and_close("PNG exports require a valid directory.");
}
}
else
{
std::filesystem::path output(path);
auto parent = output.parent_path();
auto parentInvalid =
!parent.empty() && (!std::filesystem::exists(parent, ec) || !std::filesystem::is_directory(parent, ec));
if (path.empty() || std::filesystem::is_directory(output, ec) || parentInvalid)
{
output = std::filesystem::path("output").replace_extension(render::EXTENSIONS[type]);
path = output.string();
warn_and_close(std::format("Invalid output file. Using default path: {}", path));
}
}
}
if (canStart)
{
manager.isRecordingStart = true;
playback.time = start;
playback.isPlaying = true;
renderPopup.close();
manager.progressPopup.open();
}
manager.isRecordingStart = true;
renderPopup.close();
manager.progressPopup.open();
}
ImGui::SetItemTooltip("Render the animation using the current settings.");
ImGui::SameLine();
@@ -590,107 +709,6 @@ namespace anm2ed::imgui
if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize))
{
struct Credit
{
const char* string{};
font::Type font{font::REGULAR};
};
struct ScrollingCredit
{
int index{};
float offset{};
};
struct CreditsState
{
std::vector<ScrollingCredit> active{};
float spawnTimer{1.0f};
int nextIndex{};
};
static constexpr auto ANM2ED_LABEL = "Anm2Ed";
static constexpr auto VERSION_LABEL = "Version 2.0";
static constexpr auto CREDIT_DELAY = 1.0f;
static constexpr auto CREDIT_SCROLL_SPEED = 25.0f;
static constexpr Credit CREDITS[] = {
{"Anm2Ed", font::BOLD},
{"License: GPLv3"},
{""},
{"Designer", font::BOLD},
{"Shweet"},
{""},
{"Additional Help", font::BOLD},
{"im-tem"},
{""},
{"Based on the work of:", font::BOLD},
{"Adrian Gavrilita"},
{"Simon Parzer"},
{"Matt Kapuszczak"},
{""},
{"XM Music", font::BOLD},
{"Drozerix"},
{"\"Keygen Wraith\""},
{"https://modarchive.org/module.php?207854"},
{"License: CC0"},
{""},
{"Libraries", font::BOLD},
{"Dear ImGui"},
{"https://github.com/ocornut/imgui"},
{"License: MIT"},
{""},
{"SDL"},
{"https://github.com/libsdl-org/SDL"},
{"License: zlib"},
{""},
{"SDL_mixer"},
{"https://github.com/libsdl-org/SDL_mixer"},
{"License: zlib"},
{""},
{"tinyxml2"},
{"https://github.com/leethomason/tinyxml2"},
{"License: zlib"},
{""},
{"glm"},
{"https://github.com/g-truc/glm"},
{"License: MIT"},
{""},
{"lunasvg"},
{"https://github.com/sammycage/lunasvg"},
{"License: MIT"},
{""},
{"Icons", font::BOLD},
{"Remix Icons"},
{"remixicon.com"},
{"License: Apache"},
{""},
{"Font", font::BOLD},
{"Noto Sans"},
{"https://fonts.google.com/noto/specimen/Noto+Sans"},
{"License: OFL"},
{""},
{"Special Thanks", font::BOLD},
{"Edmund McMillen"},
{"Florian Himsl"},
{"Tyrone Rodriguez"},
{"The-Vinh Truong (_kilburn)"},
{"Everyone who waited patiently for this to be finished"},
{"Everyone else who has worked on The Binding of Isaac!"},
{""},
{""},
{""},
{""},
{""},
{"enjoy the jams :)"},
{""},
{""},
{""},
{""},
{""},
};
static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(Credit));
static CreditsState creditsState{};
auto credits_reset = [&]()