Anm2 merge and adjustments to render animation
This commit is contained in:
@@ -104,7 +104,6 @@ namespace anm2ed
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
|
||||
glDeleteVertexArrays(1, &axisVAO);
|
||||
glDeleteBuffers(1, &axisVBO);
|
||||
|
||||
@@ -115,10 +114,7 @@ namespace anm2ed
|
||||
glDeleteBuffers(1, &rectVBO);
|
||||
}
|
||||
|
||||
bool Canvas::is_valid() const
|
||||
{
|
||||
return fbo != 0;
|
||||
}
|
||||
bool Canvas::is_valid() const { return fbo != 0; }
|
||||
|
||||
void Canvas::framebuffer_set() const
|
||||
{
|
||||
@@ -229,6 +225,8 @@ namespace anm2ed
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
}
|
||||
|
||||
void Canvas::rect_render(Shader& shader, const mat4& transform, const mat4& model, vec4 color, float dashLength,
|
||||
@@ -267,25 +265,40 @@ namespace anm2ed
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
void Canvas::viewport_set() const
|
||||
{
|
||||
glViewport(0, 0, size.x, size.y);
|
||||
}
|
||||
void Canvas::viewport_set() const { glViewport(0, 0, size.x, size.y); }
|
||||
|
||||
void Canvas::clear(const vec4& color) const
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
glClearColor(color.r, color.g, color.b, color.a);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_BLEND);
|
||||
}
|
||||
|
||||
void Canvas::bind() const
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
|
||||
GLboolean blendEnabled = glIsEnabled(GL_BLEND);
|
||||
if (!blendEnabled) glEnable(GL_BLEND);
|
||||
|
||||
glGetIntegerv(GL_BLEND_SRC_RGB, &previousSrcRGB);
|
||||
glGetIntegerv(GL_BLEND_DST_RGB, &previousDstRGB);
|
||||
glGetIntegerv(GL_BLEND_SRC_ALPHA, &previousSrcAlpha);
|
||||
glGetIntegerv(GL_BLEND_DST_ALPHA, &previousDstAlpha);
|
||||
previousBlendStored = true;
|
||||
|
||||
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
void Canvas::unbind() const
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (previousBlendStored)
|
||||
{
|
||||
glBlendFuncSeparate(previousSrcRGB, previousDstRGB, previousSrcAlpha, previousDstAlpha);
|
||||
previousBlendStored = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned char> Canvas::pixels_get() const
|
||||
|
||||
@@ -46,6 +46,11 @@ namespace anm2ed
|
||||
GLuint texture{};
|
||||
glm::vec2 previousSize{};
|
||||
glm::vec2 size{};
|
||||
mutable GLint previousSrcRGB{};
|
||||
mutable GLint previousDstRGB{};
|
||||
mutable GLint previousSrcAlpha{};
|
||||
mutable GLint previousDstAlpha{};
|
||||
mutable bool previousBlendStored{};
|
||||
|
||||
Canvas();
|
||||
Canvas(glm::vec2);
|
||||
|
||||
@@ -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 = [&]()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "animation_preview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <ranges>
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
@@ -14,6 +16,7 @@ using namespace anm2ed::canvas;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::resource::texture;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
@@ -26,47 +29,27 @@ namespace anm2ed::imgui
|
||||
|
||||
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
||||
|
||||
void AnimationPreview::tick(Manager& manager, Document& document, Settings& settings)
|
||||
void AnimationPreview::tick(Manager& manager, Settings& settings)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& playback = document.playback;
|
||||
auto& frameTime = document.frameTime;
|
||||
auto& end = manager.recordingEnd;
|
||||
auto& zoom = document.previewZoom;
|
||||
auto& pan = document.previewPan;
|
||||
auto& isRootTransform = settings.previewIsRootTransform;
|
||||
auto& scale = settings.renderScale;
|
||||
|
||||
if (playback.isPlaying)
|
||||
{
|
||||
auto& isSound = settings.timelineIsSound;
|
||||
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
|
||||
|
||||
if (!anm2.content.sounds.empty() && isSound)
|
||||
{
|
||||
if (auto animation = document.animation_get();
|
||||
animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
|
||||
{
|
||||
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER);
|
||||
trigger.is_visible(anm2::TRIGGER))
|
||||
if (anm2.content.sounds.contains(trigger.soundID))
|
||||
anm2.content.sounds[trigger.soundID].audio.play(false, mixer);
|
||||
}
|
||||
}
|
||||
|
||||
frameTime = playback.time;
|
||||
}
|
||||
|
||||
if (manager.isRecording)
|
||||
{
|
||||
auto& ffmpegPath = settings.renderFFmpegPath;
|
||||
auto& path = settings.renderPath;
|
||||
auto& type = settings.renderType;
|
||||
|
||||
auto pixels = pixels_get();
|
||||
renderFrames.push_back(Texture(pixels.data(), size));
|
||||
|
||||
if (playback.time > manager.recordingEnd || playback.isFinished)
|
||||
if (playback.time > end || playback.isFinished)
|
||||
{
|
||||
auto& ffmpegPath = settings.renderFFmpegPath;
|
||||
auto& path = settings.renderPath;
|
||||
auto& type = settings.renderType;
|
||||
|
||||
if (type == render::PNGS)
|
||||
{
|
||||
auto& format = settings.renderFormat;
|
||||
@@ -89,6 +72,53 @@ namespace anm2ed::imgui
|
||||
else
|
||||
toasts.warning(std::format("Could not export frames to: {}", path));
|
||||
}
|
||||
else if (type == render::SPRITESHEET)
|
||||
{
|
||||
auto& rows = settings.renderRows;
|
||||
auto& columns = settings.renderColumns;
|
||||
|
||||
if (renderFrames.empty())
|
||||
{
|
||||
toasts.warning("No frames captured for spritesheet export.");
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto& firstFrame = renderFrames.front();
|
||||
if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty())
|
||||
toasts.warning("Spritesheet export failed: captured frames are empty.");
|
||||
else
|
||||
{
|
||||
auto frameWidth = firstFrame.size.x;
|
||||
auto frameHeight = firstFrame.size.y;
|
||||
ivec2 spritesheetSize = ivec2(frameWidth * columns, frameHeight * rows);
|
||||
|
||||
std::vector<uint8_t> spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS);
|
||||
|
||||
for (auto [i, frame] : std::views::enumerate(renderFrames))
|
||||
{
|
||||
auto row = (int)(i / columns);
|
||||
auto column = (int)(i % columns);
|
||||
if (row >= rows || column >= columns) break;
|
||||
if ((int)frame.pixels.size() < frameWidth * frameHeight * CHANNELS) continue;
|
||||
|
||||
for (int y = 0; y < frameHeight; ++y)
|
||||
{
|
||||
auto destY = (size_t)(row * frameHeight + y);
|
||||
auto destX = (size_t)(column * frameWidth);
|
||||
auto destOffset = (destY * spritesheetSize.x + destX) * CHANNELS;
|
||||
auto srcOffset = (size_t)(y * frameWidth) * CHANNELS;
|
||||
std::copy_n(frame.pixels.data() + srcOffset, frameWidth * CHANNELS, spritesheet.data() + destOffset);
|
||||
}
|
||||
}
|
||||
|
||||
Texture spritesheetTexture(spritesheet.data(), spritesheetSize);
|
||||
if (spritesheetTexture.write_png(path))
|
||||
toasts.info(std::format("Exported spritesheet to: {}", path));
|
||||
else
|
||||
toasts.warning(std::format("Could not export spritesheet to: {}", path));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps))
|
||||
@@ -112,41 +142,29 @@ namespace anm2ed::imgui
|
||||
manager.progressPopup.close();
|
||||
}
|
||||
}
|
||||
if (manager.isRecordingStart)
|
||||
|
||||
if (playback.isPlaying)
|
||||
{
|
||||
savedSettings = settings;
|
||||
auto animation = document.animation_get();
|
||||
auto& isSound = settings.timelineIsSound;
|
||||
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
|
||||
|
||||
if (settings.timelineIsSound) audioStream.capture_begin(mixer);
|
||||
|
||||
if (settings.renderIsRawAnimation)
|
||||
if (!anm2.content.sounds.empty() && isSound)
|
||||
{
|
||||
settings.previewBackgroundColor = vec4();
|
||||
settings.previewIsGrid = false;
|
||||
settings.previewIsAxes = false;
|
||||
settings.timelineIsOnlyShowLayers = true;
|
||||
settings.onionskinIsEnabled = false;
|
||||
|
||||
savedZoom = zoom;
|
||||
savedPan = pan;
|
||||
|
||||
if (auto animation = document.animation_get())
|
||||
if (auto animation = document.animation_get();
|
||||
animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
|
||||
{
|
||||
if (auto rect = animation->rect(isRootTransform); rect != vec4(-1.0f))
|
||||
{
|
||||
size_set(vec2(rect.z, rect.w) * scale);
|
||||
set_to_rect(zoom, pan, rect);
|
||||
}
|
||||
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER);
|
||||
trigger.is_visible(anm2::TRIGGER))
|
||||
if (anm2.content.sounds.contains(trigger.soundID))
|
||||
anm2.content.sounds[trigger.soundID].audio.play(false, mixer);
|
||||
}
|
||||
|
||||
isSizeTrySet = false;
|
||||
|
||||
bind();
|
||||
clear(settings.previewBackgroundColor);
|
||||
unbind();
|
||||
}
|
||||
|
||||
manager.isRecordingStart = false;
|
||||
manager.isRecording = true;
|
||||
playback.tick(anm2.info.fps, animation->frameNum,
|
||||
(animation->isLoop || settings.playbackIsLoop) && !manager.isRecording);
|
||||
|
||||
frameTime = playback.time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +296,38 @@ namespace anm2ed::imgui
|
||||
|
||||
auto cursorScreenPos = ImGui::GetCursorScreenPos();
|
||||
|
||||
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;
|
||||
settings.onionskinIsEnabled = false;
|
||||
|
||||
savedZoom = zoom;
|
||||
savedPan = pan;
|
||||
|
||||
if (auto rect = document.animation_get()->rect(isRootTransform); rect != vec4(-1.0f))
|
||||
{
|
||||
size_set(vec2(rect.z, rect.w) * settings.renderScale);
|
||||
set_to_rect(zoom, pan, rect);
|
||||
}
|
||||
|
||||
isSizeTrySet = false;
|
||||
}
|
||||
|
||||
manager.isRecordingStart = false;
|
||||
manager.isRecording = true;
|
||||
playback.isPlaying = true;
|
||||
playback.time = manager.recordingStart;
|
||||
}
|
||||
|
||||
if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail()));
|
||||
viewport_set();
|
||||
bind();
|
||||
|
||||
@@ -151,7 +151,6 @@ namespace anm2ed
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glLineWidth(2.0f);
|
||||
glDisable(GL_MULTISAMPLE);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_LINE_SMOOTH);
|
||||
glClearColor(color::BLACK.r, color::BLACK.g, color::BLACK.b, color::BLACK.a);
|
||||
|
||||
Reference in New Issue
Block a user