This commit is contained in:
2026-05-14 13:30:06 +08:00
parent 974946cee4
commit e43171521d
91 changed files with 10485 additions and 3254 deletions
@@ -0,0 +1,180 @@
#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
@@ -0,0 +1,31 @@
#pragma once
#include "domain/Project.h"
#include <QHash>
#include <QPointF>
#include <QString>
namespace core {
struct AnimationSample {
QHash<QString, QPointF> entityPosition;
QHash<QString, double> entityUserScale;
QHash<QString, double> entityDepthScale01;
QHash<QString, QByteArray> entitySpritePng;
QHash<QString, bool> entityVisibility;
QHash<QString, double> entityPriority;
QHash<QString, QPointF> toolPosition;
QHash<QString, bool> toolVisibility;
QHash<QString, double> toolPriority;
QHash<QString, QPointF> cameraPosition;
QHash<QString, double> cameraScale;
};
[[nodiscard]] AnimationSample evaluateAnimation(const Project::Animation& animation, int frame);
[[nodiscard]] QVector<Project::Entity::ImageFrame> spriteFramesForEntity(const Project::Animation& animation,
const QString& entityId);
} // namespace core