diff --git a/.vscode/launch.json b/.vscode/launch.json index dd8dffe..f29b4f0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Debug", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build/anm2ed", + "program": "${workspaceFolder}/out/build/linux-debug/anm2ed", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -28,7 +28,7 @@ "name": "Release", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build-release/anm2ed", + "program": "${workspaceFolder}/out/build/linux-release/anm2ed", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 98e27be..3ac1dec 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,10 +1,18 @@ { "version": "2.0.0", "tasks": [ + { + "label": "run-debug", + "type": "shell", + "command": "cmake -S . -B out/build/linux-debug -DCMAKE_BUILD_TYPE=Debug && cmake --build out/build/linux-debug --parallel 8 --target anm2ed && ./out/build/linux-debug/anm2ed", + "problemMatcher": [ + "$gcc" + ] + }, { "label": "build", "type": "shell", - "command": "cmake --build build --target anm2ed", + "command": "cmake -S . -B out/build/linux-debug -DCMAKE_BUILD_TYPE=Debug && cmake --build out/build/linux-debug --parallel 8 --target anm2ed", "group": { "kind": "build", "isDefault": true @@ -13,14 +21,22 @@ "$gcc" ] }, + { + "label": "run-release", + "type": "shell", + "command": "cmake -S . -B out/build/linux-release -DCMAKE_BUILD_TYPE=Release && cmake --build out/build/linux-release --parallel 8 --target anm2ed && ./out/build/linux-release/anm2ed", + "problemMatcher": [ + "$gcc" + ] + }, { "label": "build-release", "type": "shell", - "command": "cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release && cmake --build build-release --target anm2ed", + "command": "cmake -S . -B out/build/linux-release -DCMAKE_BUILD_TYPE=Release && cmake --build out/build/linux-release --parallel 8 --target anm2ed", "group": "build", "problemMatcher": [ "$gcc" ] } ] -} +} \ No newline at end of file diff --git a/README.md b/README.md index a8303a6..868f536 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ A reimplementation of *The Binding of Isaac: Rebirth*'s proprietary animation editor. Manipulates the XML-based ".anm2" format, used for in-game tweened animations. +### Clarification +This application was partly vibe-coded by an LLM (let's say 70% me/30% AI sloppa). Sorry not sorry, I'm just lazy. + ## Features - Extended version of the original proprietary Nicalis animation editor - Smooth [Dear ImGui](https://github.com/ocornut/imgui) interface; docking, dragging and dropping, etc. You might be familiar with it from [Repentogon](https://repentogon.com/). diff --git a/compile_commands.json b/compile_commands.json index 5e95284..bc0dce8 120000 --- a/compile_commands.json +++ b/compile_commands.json @@ -1 +1 @@ -/home/anon/sda/Personal/Repos/anm2ed/build/compile_commands.json \ No newline at end of file +/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-debug/compile_commands.json \ No newline at end of file diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index 9c80f54..c642f5f 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -18,6 +18,13 @@ using namespace glm; namespace { + int remap_id(const std::unordered_map& table, int value) + { + if (value < 0) return value; + if (auto it = table.find(value); it != table.end()) return it->second; + return value; + } + void region_frames_sync(anm2ed::anm2::Anm2& anm2, bool clearInvalid) { for (auto& animation : anm2.animations.items) @@ -84,12 +91,13 @@ namespace anm2ed::anm2 XMLElement* Anm2::to_element(XMLDocument& document, Flags flags) { - region_frames_sync(*this, true); + auto normalized = normalized_for_serialize(); + region_frames_sync(normalized, true); auto element = document.NewElement("AnimatedActor"); - info.serialize(document, element); - content.serialize(document, element, flags); - animations.serialize(document, element, flags); + normalized.info.serialize(document, element); + normalized.content.serialize(document, element, flags); + normalized.animations.serialize(document, element, flags); return element; } @@ -123,6 +131,46 @@ namespace anm2ed::anm2 uint64_t Anm2::hash() { return std::hash{}(to_string()); } + Anm2 Anm2::normalized_for_serialize() const + { + auto normalized = *this; + std::unordered_map layerRemap{}; + + int normalizedID = 0; + for (auto& [layerID, layer] : content.layers) + { + layerRemap[layerID] = normalizedID; + ++normalizedID; + } + + normalized.content.layers.clear(); + for (auto& [layerID, layer] : content.layers) + normalized.content.layers[remap_id(layerRemap, layerID)] = layer; + + for (auto& animation : normalized.animations.items) + { + std::unordered_map layerAnimations{}; + std::vector layerOrder{}; + + for (auto layerID : animation.layerOrder) + { + auto mappedID = remap_id(layerRemap, layerID); + if (mappedID >= 0) layerOrder.push_back(mappedID); + } + + for (auto& [layerID, item] : animation.layerAnimations) + { + auto mappedID = remap_id(layerRemap, layerID); + if (mappedID >= 0) layerAnimations[mappedID] = item; + } + + animation.layerAnimations = std::move(layerAnimations); + animation.layerOrder = std::move(layerOrder); + } + + return normalized; + } + Frame* Anm2::frame_get(int animationIndex, Type itemType, int frameIndex, int itemID) { if (auto item = item_get(animationIndex, itemType, itemID); item) @@ -177,13 +225,6 @@ namespace anm2ed::anm2 return original; }; - auto remap_id = [](const auto& table, int value) - { - if (value < 0) return value; - if (auto it = table.find(value); it != table.end()) return it->second; - return value; - }; - std::unordered_map spritesheetRemap{}; std::unordered_map layerRemap{}; std::unordered_map nullRemap{}; diff --git a/src/anm2/anm2.h b/src/anm2/anm2.h index e931450..0469dbd 100644 --- a/src/anm2/anm2.h +++ b/src/anm2/anm2.h @@ -36,6 +36,7 @@ namespace anm2ed::anm2 std::string to_string(Flags = 0); Anm2(const std::filesystem::path&, std::string* = nullptr); uint64_t hash(); + Anm2 normalized_for_serialize() const; Spritesheet* spritesheet_get(int); bool spritesheet_add(const std::filesystem::path&, const std::filesystem::path&, int&); @@ -80,7 +81,7 @@ namespace anm2ed::anm2 bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); Item* item_get(int, Type, int = -1); - Reference layer_animation_add(Reference = {}, std::string = {}, int = 0, + Reference layer_animation_add(Reference = {}, int = -1, std::string = {}, int = 0, types::destination::Type = types::destination::ALL); Reference null_animation_add(Reference = {}, std::string = {}, bool = false, types::destination::Type = types::destination::ALL); diff --git a/src/anm2/anm2_items.cpp b/src/anm2/anm2_items.cpp index aed2f7b..712d4c8 100644 --- a/src/anm2/anm2_items.cpp +++ b/src/anm2/anm2_items.cpp @@ -30,7 +30,7 @@ namespace anm2ed::anm2 return nullptr; } - Reference Anm2::layer_animation_add(Reference reference, std::string name, int spritesheetID, + Reference Anm2::layer_animation_add(Reference reference, int insertBeforeID, std::string name, int spritesheetID, destination::Type destination) { auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID; @@ -39,21 +39,35 @@ namespace anm2ed::anm2 layer.name = !name.empty() ? name : layer.name; layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0; - auto add = [&](Animation* animation, int id) + auto add = [&](Animation* animation, int id, bool insertBeforeReference) { animation->layerAnimations[id] = Item(); + + if (insertBeforeReference && insertBeforeID != -1) + { + auto it = std::find(animation->layerOrder.begin(), animation->layerOrder.end(), insertBeforeID); + if (it != animation->layerOrder.end()) + { + animation->layerOrder.insert(it, id); + return; + } + } + animation->layerOrder.push_back(id); }; if (destination == destination::ALL) { - for (auto& animation : animations.items) - if (!animation.layerAnimations.contains(id)) add(&animation, id); + for (size_t index = 0; index < animations.items.size(); ++index) + { + auto& animation = animations.items[index]; + if (!animation.layerAnimations.contains(id)) add(&animation, id, true); + } } else if (destination == destination::THIS) { if (auto animation = animation_get(reference.animationIndex)) - if (!animation->layerAnimations.contains(id)) add(animation, id); + if (!animation->layerAnimations.contains(id)) add(animation, id, true); } return {reference.animationIndex, LAYER, id}; @@ -82,4 +96,4 @@ namespace anm2ed::anm2 return {reference.animationIndex, NULL_, id}; } -} \ No newline at end of file +} diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index 736c882..52ec3c3 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -104,9 +104,10 @@ namespace anm2ed::anm2 { bool noRegions = has_flag(flags, NO_REGIONS); bool frameNoRegionValues = has_flag(flags, FRAME_NO_REGION_VALUES); - bool writeRegionValues = !frameNoRegionValues || noRegions; + bool hasValidRegion = !noRegions && regionID != -1; + bool writeRegionValues = !frameNoRegionValues || !hasValidRegion; - if (!noRegions && regionID != -1) element->SetAttribute("RegionId", regionID); + if (hasValidRegion) element->SetAttribute("RegionId", regionID); element->SetAttribute("XPosition", position.x); element->SetAttribute("YPosition", position.y); if (writeRegionValues) diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index e821ba3..f530b20 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -82,6 +82,27 @@ namespace anm2ed::imgui directory.clear(); frames.clear(); } + + void pixels_unpremultiply_alpha(std::vector& pixels) + { + for (size_t index = 0; index + 3 < pixels.size(); index += 4) + { + auto alpha = pixels[index + 3]; + if (alpha == 0) + { + pixels[index + 0] = 0; + pixels[index + 1] = 0; + pixels[index + 2] = 0; + continue; + } + if (alpha == 255) continue; + + float alphaUnit = (float)alpha / 255.0f; + pixels[index + 0] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 0] / alphaUnit), 0.0f, 255.0f); + pixels[index + 1] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 1] / alphaUnit), 0.0f, 255.0f); + pixels[index + 2] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 2] / alphaUnit), 0.0f, 255.0f); + } + } } AnimationPreview::AnimationPreview() : Canvas(vec2()) {} @@ -232,6 +253,7 @@ namespace anm2ed::imgui bind(); auto pixels = pixels_get(); + if (settings.renderIsRawAnimation) pixels_unpremultiply_alpha(pixels); auto frameIndex = (int)renderTempFrames.size(); auto framePath = renderTempDirectory / render_frame_filename(settings.renderFormat, frameIndex); if (Texture::write_pixels_png(framePath, size, pixels.data())) @@ -240,7 +262,8 @@ namespace anm2ed::imgui } else { - toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); + toasts.push( + std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); if (type != render::PNGS) render_temp_cleanup(renderTempDirectory, renderTempFrames); @@ -513,7 +536,8 @@ namespace anm2ed::imgui if (renderTempDirectory.empty()) { auto pathString = path::to_utf8(settings.renderPath); - toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); + toasts.push( + std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); manager.isRecording = false; diff --git a/src/imgui/window/regions.cpp b/src/imgui/window/regions.cpp index 8be4840..e3e6c44 100644 --- a/src/imgui/window/regions.cpp +++ b/src/imgui/window/regions.cpp @@ -3,14 +3,12 @@ #include #include -#include #include #include "document.h" #include "log.h" #include "map_.h" #include "math_.h" -#include "path_.h" #include "strings.h" #include "toast.h" #include "vector_.h" diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 2c206c7..f8e632e 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -1780,11 +1780,12 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) { anm2::Reference addReference{}; + int insertBeforeID = reference.itemType == anm2::LAYER ? reference.itemID : -1; document.snapshot(localize.get(EDIT_ADD_ITEM)); if (type == anm2::LAYER) - addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, - addItemSpritesheetID, (destination::Type)destination); + addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, insertBeforeID, + addItemName, addItemSpritesheetID, (destination::Type)destination); else if (type == anm2::NULL_) addReference = anm2.null_animation_add({reference.animationIndex, anm2::NULL_, addItemID}, addItemName, addItemIsShowRect, (destination::Type)destination); diff --git a/src/render.cpp b/src/render.cpp index 9149458..e14a2ee 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -133,7 +133,8 @@ namespace anm2ed { case render::GIF: command += - " -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full[p];[s1][p]paletteuse=dither=floyd_steinberg\"" + " -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full:reserve_transparent=1[p];" + "[s1][p]paletteuse=dither=floyd_steinberg:alpha_threshold=128\"" " -loop 0"; command += std::format(" \"{}\"", pathString); break;