I N T E R P O L A T I O N

This commit is contained in:
2026-04-14 14:28:14 -04:00
parent 15f85b84a9
commit b60c4bc295
25 changed files with 483 additions and 172 deletions
+27 -2
View File
@@ -124,6 +124,28 @@ namespace anm2ed::anm2
return {minX, minY, maxX - minX, maxY - minY};
}
void Anm2::bake_special_interpolated_frames(int interval, bool isRoundScale, bool isRoundRotation)
{
auto bake_item = [&](Item& item)
{
for (int i = (int)item.frames.size() - 1; i >= 0; --i)
{
auto interpolation = item.frames[i].interpolation;
if (interpolation == Frame::Interpolation::NONE || interpolation == Frame::Interpolation::LINEAR) continue;
item.frames_bake(i, interval, isRoundScale, isRoundRotation);
}
};
for (auto& animation : animations.items)
{
bake_item(animation.rootAnimation);
for (auto& item : animation.layerAnimations | std::views::values)
bake_item(item);
for (auto& item : animation.nullAnimations | std::views::values)
bake_item(item);
}
}
Anm2::Anm2(const std::filesystem::path& path, std::string* errorString)
{
XMLDocument document;
@@ -168,10 +190,13 @@ namespace anm2ed::anm2
return element;
}
bool Anm2::serialize(const std::filesystem::path& path, std::string* errorString, Flags flags)
bool Anm2::serialize(const std::filesystem::path& path, std::string* errorString, Flags flags,
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
{
XMLDocument document;
document.InsertFirstChild(to_element(document, flags));
auto serialized = *this;
if (isBakeSpecialInterpolatedFramesOnSave) serialized.bake_special_interpolated_frames(1, isRoundScale, isRoundRotation);
document.InsertFirstChild(serialized.to_element(document, flags));
File file(path, "wb");
if (!file)
+3 -1
View File
@@ -32,7 +32,8 @@ namespace anm2ed::anm2
Anm2();
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
bool serialize(const std::filesystem::path&, std::string* = nullptr, Flags = 0);
bool serialize(const std::filesystem::path&, std::string* = nullptr, Flags = 0, bool = false, bool = true,
bool = true);
std::string to_string(Flags = 0);
Anm2(const std::filesystem::path&, std::string* = nullptr);
uint64_t hash();
@@ -81,6 +82,7 @@ namespace anm2ed::anm2
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
Frame frame_effective(int, const Frame&) const;
glm::vec4 animation_rect(Animation&, bool) const;
void bake_special_interpolated_frames(int, bool, bool);
Item* item_get(int, Type, int = -1);
Reference layer_animation_add(Reference = {}, int = -1, std::string = {}, int = 0,
+5 -2
View File
@@ -106,7 +106,8 @@ namespace anm2ed::anm2
{
NO_SOUNDS = 1 << 0,
NO_REGIONS = 1 << 1,
FRAME_NO_REGION_VALUES = 1 << 2
FRAME_NO_REGION_VALUES = 1 << 2,
INTERPOLATION_BOOL_ONLY = 1 << 3
};
typedef int Flags;
@@ -114,5 +115,7 @@ namespace anm2ed::anm2
inline bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; }
inline const std::unordered_map<Compatibility, Flags> COMPATIBILITY_FLAGS = {
{ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES}, {ANM2ED, 0}, {ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}};
{ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES | INTERPOLATION_BOOL_ONLY},
{ANM2ED, 0},
{ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}};
}
+48 -4
View File
@@ -1,5 +1,7 @@
#include "frame.hpp"
#include <cstring>
#include "math_.hpp"
#include "xml_.hpp"
@@ -8,8 +10,40 @@ using namespace tinyxml2;
namespace anm2ed::anm2
{
namespace
{
Frame::Interpolation interpolation_from_xml(const char* value, bool fallback)
{
if (value)
{
if (std::strcmp(value, "EaseIn") == 0) return Frame::Interpolation::EASE_IN;
if (std::strcmp(value, "EaseOut") == 0) return Frame::Interpolation::EASE_OUT;
if (std::strcmp(value, "EaseInOut") == 0) return Frame::Interpolation::EASE_IN_OUT;
}
return fallback ? Frame::Interpolation::LINEAR : Frame::Interpolation::NONE;
}
const char* interpolation_to_xml(Frame::Interpolation interpolation)
{
switch (interpolation)
{
case Frame::Interpolation::EASE_IN:
return "EaseIn";
case Frame::Interpolation::EASE_OUT:
return "EaseOut";
case Frame::Interpolation::EASE_IN_OUT:
return "EaseInOut";
default:
return nullptr;
}
}
}
Frame::Frame(XMLElement* element, Type type)
{
bool isInterpolatedBool{};
const char* interpolationValue = element->Attribute("Interpolated");
switch (type)
{
case ROOT:
@@ -28,7 +62,8 @@ namespace anm2ed::anm2
xml::query_color_attribute(element, "GreenOffset", colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
element->QueryBoolAttribute("Interpolated", &isInterpolatedBool);
if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool);
break;
case LAYER:
element->QueryIntAttribute("RegionId", &regionID);
@@ -52,7 +87,8 @@ namespace anm2ed::anm2
xml::query_color_attribute(element, "GreenOffset", colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
element->QueryBoolAttribute("Interpolated", &isInterpolatedBool);
if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool);
break;
case TRIGGER:
{
@@ -98,7 +134,11 @@ namespace anm2ed::anm2
element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g));
element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b));
element->SetAttribute("Rotation", rotation);
element->SetAttribute("Interpolated", isInterpolated);
if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE ||
interpolation == Interpolation::LINEAR)
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
else if (const char* interpolationValue = interpolation_to_xml(interpolation))
element->SetAttribute("Interpolated", interpolationValue);
break;
case LAYER:
{
@@ -131,7 +171,11 @@ namespace anm2ed::anm2
element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g));
element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b));
element->SetAttribute("Rotation", rotation);
element->SetAttribute("Interpolated", isInterpolated);
if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE ||
interpolation == Interpolation::LINEAR)
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
else if (const char* interpolationValue = interpolation_to_xml(interpolation))
element->SetAttribute("Interpolated", interpolationValue);
break;
}
case TRIGGER:
+11 -2
View File
@@ -15,8 +15,17 @@ namespace anm2ed::anm2
class Frame
{
public:
enum Interpolation
{
NONE,
LINEAR,
EASE_IN,
EASE_OUT,
EASE_IN_OUT
};
bool isVisible{true};
bool isInterpolated{false};
Interpolation interpolation{NONE};
float rotation{};
int duration{FRAME_DURATION_MIN};
int atFrame{-1};
@@ -43,7 +52,7 @@ namespace anm2ed::anm2
struct FrameChange
{
std::optional<bool> isVisible{};
std::optional<bool> isInterpolated{};
std::optional<Frame::Interpolation> interpolation{};
std::optional<float> rotation{};
std::optional<int> duration{};
std::optional<int> regionID{};
+30 -5
View File
@@ -1,5 +1,6 @@
#include "item.hpp"
#include <algorithm>
#include <cmath>
#include <ranges>
#include "vector_.hpp"
@@ -11,6 +12,29 @@ using namespace glm;
namespace anm2ed::anm2
{
namespace
{
float interpolation_factor(Frame::Interpolation interpolation, float value)
{
value = glm::clamp(value, 0.0f, 1.0f);
switch (interpolation)
{
case Frame::Interpolation::LINEAR:
return value;
case Frame::Interpolation::EASE_IN:
return value * value;
case Frame::Interpolation::EASE_OUT:
return 1.0f - ((1.0f - value) * (1.0f - value));
case Frame::Interpolation::EASE_IN_OUT:
return value < 0.5f ? (2.0f * value * value) : (1.0f - std::pow(-2.0f * value + 2.0f, 2.0f) * 0.5f);
case Frame::Interpolation::NONE:
default:
return 0.0f;
}
}
}
Item::Item(XMLElement* element, Type type, int* id)
{
if (type == LAYER && id) element->QueryIntAttribute("LayerId", id);
@@ -112,9 +136,10 @@ namespace anm2ed::anm2
}
}
if (type != TRIGGER && frame.isInterpolated && frameNext && frame.duration > 1)
if (type != TRIGGER && frame.interpolation != Frame::Interpolation::NONE && frameNext && frame.duration > 1)
{
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
auto interpolation =
interpolation_factor(frame.interpolation, (time - durationCurrent) / (durationNext - durationCurrent));
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation);
frame.position = glm::mix(frame.position, frameNext->position, interpolation);
@@ -169,7 +194,7 @@ namespace anm2ed::anm2
Frame& frame = frames[i];
if (change.isVisible) frame.isVisible = *change.isVisible;
if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated;
if (change.interpolation) frame.interpolation = *change.interpolation;
if (change.isFlipX) frame.scale.x = -frame.scale.x;
if (change.isFlipY) frame.scale.y = -frame.scale.y;
@@ -280,9 +305,9 @@ namespace anm2ed::anm2
while (duration < original.duration)
{
Frame baked = original;
float interpolation = (float)duration / original.duration;
float interpolation = interpolation_factor(original.interpolation, (float)duration / original.duration);
baked.duration = std::min(interval, original.duration - duration);
baked.isInterpolated = (i == index) ? original.isInterpolated : false;
baked.interpolation = (i == index) ? original.interpolation : Frame::Interpolation::NONE;
baked.rotation = glm::mix(original.rotation, nextFrame.rotation, interpolation);
baked.position = glm::mix(original.position, nextFrame.position, interpolation);
baked.scale = glm::mix(original.scale, nextFrame.scale, interpolation);