181 lines
7.6 KiB
C++
181 lines
7.6 KiB
C++
#include "animation/AnimationEvaluator.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace core {
|
|
namespace {
|
|
|
|
QVector<Project::AnimationKey> sortedKeys(const QVector<Project::AnimationKey>& keys) {
|
|
QVector<Project::AnimationKey> out = keys;
|
|
std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
|
|
return out;
|
|
}
|
|
|
|
bool sampleBool(const QVector<Project::AnimationKey>& keys, int frame, bool fallback) {
|
|
auto sorted = sortedKeys(keys);
|
|
bool out = fallback;
|
|
for (const auto& k : sorted) {
|
|
if (k.frame <= frame) out = k.boolValue;
|
|
else break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
QString sampleString(const QVector<Project::AnimationKey>& keys, int frame, const QString& fallback = {}) {
|
|
auto sorted = sortedKeys(keys);
|
|
QString out = fallback;
|
|
for (const auto& k : sorted) {
|
|
if (k.frame <= frame && !k.stringValue.isEmpty()) out = k.stringValue;
|
|
else if (k.frame > frame) break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
QByteArray sampleBytes(const QVector<Project::AnimationKey>& keys, int frame, const QByteArray& fallback = {}) {
|
|
auto sorted = sortedKeys(keys);
|
|
QByteArray out = fallback;
|
|
for (const auto& k : sorted) {
|
|
if (k.frame <= frame && !k.bytesValue.isEmpty()) out = k.bytesValue;
|
|
else if (k.frame > frame) break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
double sampleDouble(const QVector<Project::AnimationKey>& keys,
|
|
int frame,
|
|
double fallback,
|
|
Project::AnimationInterpolation interpolation) {
|
|
auto sorted = sortedKeys(keys);
|
|
if (sorted.isEmpty()) return fallback;
|
|
if (interpolation == Project::AnimationInterpolation::Hold) {
|
|
double out = fallback;
|
|
for (const auto& k : sorted) {
|
|
if (k.frame <= frame) out = k.numberValue;
|
|
else break;
|
|
}
|
|
return out;
|
|
}
|
|
if (frame <= sorted.front().frame) return sorted.front().numberValue;
|
|
if (frame >= sorted.back().frame) return sorted.back().numberValue;
|
|
for (int i = 0; i + 1 < sorted.size(); ++i) {
|
|
const auto& a = sorted[i];
|
|
const auto& b = sorted[i + 1];
|
|
if (frame >= a.frame && frame <= b.frame) {
|
|
if (a.frame == b.frame) return a.numberValue;
|
|
const double t = double(frame - a.frame) / double(b.frame - a.frame);
|
|
return a.numberValue + (b.numberValue - a.numberValue) * t;
|
|
}
|
|
}
|
|
return sorted.back().numberValue;
|
|
}
|
|
|
|
QPointF sampleVec2(const QVector<Project::AnimationKey>& keys,
|
|
int frame,
|
|
const QPointF& fallback,
|
|
Project::AnimationInterpolation interpolation) {
|
|
auto sorted = sortedKeys(keys);
|
|
if (sorted.isEmpty()) return fallback;
|
|
if (interpolation == Project::AnimationInterpolation::Hold) {
|
|
QPointF out = fallback;
|
|
for (const auto& k : sorted) {
|
|
if (k.frame <= frame) out = k.vec2Value;
|
|
else break;
|
|
}
|
|
return out;
|
|
}
|
|
if (frame <= sorted.front().frame) return sorted.front().vec2Value;
|
|
if (frame >= sorted.back().frame) return sorted.back().vec2Value;
|
|
for (int i = 0; i + 1 < sorted.size(); ++i) {
|
|
const auto& a = sorted[i];
|
|
const auto& b = sorted[i + 1];
|
|
if (frame >= a.frame && frame <= b.frame) {
|
|
if (a.frame == b.frame) return a.vec2Value;
|
|
const double t = double(frame - a.frame) / double(b.frame - a.frame);
|
|
return QPointF(a.vec2Value.x() + (b.vec2Value.x() - a.vec2Value.x()) * t,
|
|
a.vec2Value.y() + (b.vec2Value.y() - a.vec2Value.y()) * t);
|
|
}
|
|
}
|
|
return sorted.back().vec2Value;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
AnimationSample evaluateAnimation(const Project::Animation& animation, int frame) {
|
|
AnimationSample out;
|
|
const int f = animation.loop && animation.lengthFrames > 0
|
|
? (std::max(0, frame) % animation.lengthFrames)
|
|
: std::clamp(frame, 0, std::max(0, animation.lengthFrames - 1));
|
|
|
|
for (const auto& t : animation.tracks) {
|
|
if (t.targetId.isEmpty() || t.keys.isEmpty()) continue;
|
|
if (t.targetKind == Project::AnimationTargetKind::Entity) {
|
|
switch (t.property) {
|
|
case Project::AnimationProperty::Position:
|
|
out.entityPosition.insert(t.targetId, sampleVec2(t.keys, f, QPointF(), t.interpolation));
|
|
break;
|
|
case Project::AnimationProperty::UserScale:
|
|
out.entityUserScale.insert(t.targetId, sampleDouble(t.keys, f, 1.0, t.interpolation));
|
|
break;
|
|
case Project::AnimationProperty::DepthScale:
|
|
out.entityDepthScale01.insert(t.targetId, sampleDouble(t.keys, f, 0.5, t.interpolation));
|
|
break;
|
|
case Project::AnimationProperty::Sprite:
|
|
// 贴图仅支持二进制 PNG bytes(不兼容路径关键帧)。
|
|
if (t.valueType == Project::AnimationValueType::PngBytes) {
|
|
out.entitySpritePng.insert(t.targetId, sampleBytes(t.keys, f));
|
|
}
|
|
break;
|
|
case Project::AnimationProperty::Visibility:
|
|
out.entityVisibility.insert(t.targetId, sampleBool(t.keys, f, true));
|
|
break;
|
|
case Project::AnimationProperty::Priority:
|
|
out.entityPriority.insert(t.targetId, sampleDouble(t.keys, f, 1.0, t.interpolation));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else if (t.targetKind == Project::AnimationTargetKind::Tool) {
|
|
if (t.property == Project::AnimationProperty::Position) {
|
|
out.toolPosition.insert(t.targetId, sampleVec2(t.keys, f, QPointF(), t.interpolation));
|
|
} else if (t.property == Project::AnimationProperty::Visibility) {
|
|
out.toolVisibility.insert(t.targetId, sampleBool(t.keys, f, true));
|
|
} else if (t.property == Project::AnimationProperty::Priority) {
|
|
out.toolPriority.insert(t.targetId, sampleDouble(t.keys, f, 10.0, t.interpolation));
|
|
}
|
|
} else if (t.targetKind == Project::AnimationTargetKind::Camera) {
|
|
if (t.property == Project::AnimationProperty::Position) {
|
|
out.cameraPosition.insert(t.targetId, sampleVec2(t.keys, f, QPointF(), t.interpolation));
|
|
} else if (t.property == Project::AnimationProperty::CameraScale) {
|
|
out.cameraScale.insert(t.targetId, sampleDouble(t.keys, f, 1.0, t.interpolation));
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
QVector<Project::Entity::ImageFrame> spriteFramesForEntity(const Project::Animation& animation,
|
|
const QString& entityId) {
|
|
QVector<Project::Entity::ImageFrame> out;
|
|
for (const auto& t : animation.tracks) {
|
|
if (t.targetKind != Project::AnimationTargetKind::Entity ||
|
|
t.targetId != entityId ||
|
|
t.property != Project::AnimationProperty::Sprite) {
|
|
continue;
|
|
}
|
|
out.reserve(out.size() + t.keys.size());
|
|
for (const auto& k : t.keys) {
|
|
if (t.valueType == Project::AnimationValueType::PngBytes) {
|
|
if (!k.bytesValue.isEmpty()) {
|
|
out.push_back(Project::Entity::ImageFrame{k.frame, QString()});
|
|
}
|
|
} else if (t.valueType == Project::AnimationValueType::ImagePath) {
|
|
// 旧格式:不再支持路径型 sprite 关键帧(按“无/回退默认贴图”处理)
|
|
}
|
|
}
|
|
}
|
|
std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
|
|
return out;
|
|
}
|
|
|
|
} // namespace core
|