fix: apply character bubble anchors
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"bubble": {
|
||||
"offsetX": 0,
|
||||
"offsetY": -180
|
||||
"offsetY": -475
|
||||
},
|
||||
"states": {
|
||||
"idle": {
|
||||
|
||||
@@ -14,6 +14,21 @@ struct CharacterState
|
||||
QString nextState;
|
||||
};
|
||||
|
||||
struct CharacterBase
|
||||
{
|
||||
int width = 320;
|
||||
int height = 320;
|
||||
double scale = 1.0;
|
||||
double anchorX = 0.5;
|
||||
double anchorY = 0.0;
|
||||
};
|
||||
|
||||
struct CharacterBubble
|
||||
{
|
||||
double offsetX = 0.0;
|
||||
double offsetY = -8.0;
|
||||
};
|
||||
|
||||
class CharacterPackage
|
||||
{
|
||||
public:
|
||||
@@ -25,9 +40,10 @@ public:
|
||||
QString packagePath;
|
||||
QString previewPath;
|
||||
QString defaultState;
|
||||
CharacterBase base;
|
||||
CharacterBubble bubble;
|
||||
QMap<QString, CharacterState> states;
|
||||
|
||||
bool hasState(const QString &stateName) const;
|
||||
const CharacterState *state(const QString &stateName) const;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -21,6 +22,62 @@ QString requiredString(const QJsonObject &object, const QString &key)
|
||||
|
||||
return value.toString().trimmed();
|
||||
}
|
||||
|
||||
int optionalPositiveInt(const QJsonObject &object, const QString &key, int fallback)
|
||||
{
|
||||
const int value = object.value(key).toInt(fallback);
|
||||
return value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
double optionalPositiveDouble(const QJsonObject &object, const QString &key, double fallback)
|
||||
{
|
||||
const double value = object.value(key).toDouble(fallback);
|
||||
return value > 0.0 ? value : fallback;
|
||||
}
|
||||
|
||||
double optionalNormalizedDouble(const QJsonObject &object, const QString &key, double fallback)
|
||||
{
|
||||
const double value = object.value(key).toDouble(fallback);
|
||||
return qBound(0.0, value, 1.0);
|
||||
}
|
||||
|
||||
double optionalDouble(const QJsonObject &object, const QString &key, double fallback)
|
||||
{
|
||||
return object.value(key).toDouble(fallback);
|
||||
}
|
||||
|
||||
CharacterBase parseBase(const QJsonObject &root)
|
||||
{
|
||||
CharacterBase base;
|
||||
const QJsonValue baseValue = root.value(QStringLiteral("base"));
|
||||
if (!baseValue.isObject())
|
||||
{
|
||||
return base;
|
||||
}
|
||||
|
||||
const QJsonObject baseObject = baseValue.toObject();
|
||||
base.width = optionalPositiveInt(baseObject, QStringLiteral("width"), base.width);
|
||||
base.height = optionalPositiveInt(baseObject, QStringLiteral("height"), base.height);
|
||||
base.scale = optionalPositiveDouble(baseObject, QStringLiteral("scale"), base.scale);
|
||||
base.anchorX = optionalNormalizedDouble(baseObject, QStringLiteral("anchorX"), base.anchorX);
|
||||
base.anchorY = optionalNormalizedDouble(baseObject, QStringLiteral("anchorY"), base.anchorY);
|
||||
return base;
|
||||
}
|
||||
|
||||
CharacterBubble parseBubble(const QJsonObject &root)
|
||||
{
|
||||
CharacterBubble bubble;
|
||||
const QJsonValue bubbleValue = root.value(QStringLiteral("bubble"));
|
||||
if (!bubbleValue.isObject())
|
||||
{
|
||||
return bubble;
|
||||
}
|
||||
|
||||
const QJsonObject bubbleObject = bubbleValue.toObject();
|
||||
bubble.offsetX = optionalDouble(bubbleObject, QStringLiteral("offsetX"), bubble.offsetX);
|
||||
bubble.offsetY = optionalDouble(bubbleObject, QStringLiteral("offsetY"), bubble.offsetY);
|
||||
return bubble;
|
||||
}
|
||||
}
|
||||
|
||||
CharacterPackage CharacterPackageLoader::load(const QString &packagePath, QString *errorMessage)
|
||||
@@ -69,6 +126,8 @@ CharacterPackage CharacterPackageLoader::load(const QString &packagePath, QStrin
|
||||
package.author = requiredString(root, QStringLiteral("author"));
|
||||
package.version = requiredString(root, QStringLiteral("version"));
|
||||
package.defaultState = requiredString(root, QStringLiteral("defaultState"));
|
||||
package.base = parseBase(root);
|
||||
package.bubble = parseBubble(root);
|
||||
|
||||
if (package.id.isEmpty())
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace
|
||||
constexpr int MinBubbleWidth = 120;
|
||||
constexpr int MaxBubbleWidth = 420;
|
||||
constexpr int MaxBubbleHeight = 220;
|
||||
constexpr int BubbleOffsetY = 8;
|
||||
constexpr int BubblePaddingWidth = 28;
|
||||
constexpr int BubblePaddingHeight = 24;
|
||||
constexpr int BubbleWidthSafetyMargin = 12;
|
||||
@@ -98,6 +97,7 @@ void ChatBubble::showMessage(const QString &message, const QPoint &anchorPositio
|
||||
setFixedSize(bubbleSize);
|
||||
updatePosition();
|
||||
show();
|
||||
raise();
|
||||
|
||||
m_textEdit->verticalScrollBar()->setValue(
|
||||
scrollToBottom ? m_textEdit->verticalScrollBar()->maximum() : 0);
|
||||
@@ -223,5 +223,5 @@ QSize ChatBubble::preferredBubbleSize(const QString &message) const
|
||||
|
||||
void ChatBubble::updatePosition()
|
||||
{
|
||||
move(m_anchorPosition.x() - width() / 2, m_anchorPosition.y() - height() - BubbleOffsetY);
|
||||
move(m_anchorPosition.x() - width() / 2, m_anchorPosition.y() - height());
|
||||
}
|
||||
|
||||
+28
-5
@@ -21,6 +21,7 @@
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QPixmap>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#include <QRandomGenerator>
|
||||
#include <QScreen>
|
||||
@@ -46,10 +47,20 @@ QString previewImagePath()
|
||||
constexpr int MaxUserMessageLength = 4000;
|
||||
constexpr int ChatInputLowerOffsetY = 48;
|
||||
constexpr int StreamBubbleUpdateIntervalMs = 80;
|
||||
constexpr int BaseAnimationTargetSize = 320;
|
||||
constexpr int MinAnimationTargetSide = 32;
|
||||
constexpr int MaxAnimationTargetSide = 2048;
|
||||
constexpr int LowPowerFpsCap = 6;
|
||||
constexpr int ChatFinishedReturnDelayMs = 1500;
|
||||
|
||||
int boundedAnimationTargetSide(double sideLength)
|
||||
{
|
||||
const double boundedSideLength = qBound(
|
||||
static_cast<double>(MinAnimationTargetSide),
|
||||
sideLength,
|
||||
static_cast<double>(MaxAnimationTargetSide));
|
||||
return qRound(boundedSideLength);
|
||||
}
|
||||
|
||||
int evenBoundedHistoryLimit(int value, int minimum, int maximum)
|
||||
{
|
||||
const int boundedValue = qBound(minimum, value, maximum);
|
||||
@@ -801,7 +812,17 @@ void PetWindow::updateBubblePosition()
|
||||
|
||||
QPoint PetWindow::bubbleAnchorPosition() const
|
||||
{
|
||||
return frameGeometry().topLeft() + QPoint(width() / 2, 0);
|
||||
const CharacterBase &base = m_characterPackage.base;
|
||||
const CharacterBubble &bubble = m_characterPackage.bubble;
|
||||
const double baseWidth = base.width > 0 ? static_cast<double>(base.width) : static_cast<double>(width());
|
||||
const double baseHeight = base.height > 0 ? static_cast<double>(base.height) : static_cast<double>(height());
|
||||
const double scaleX = baseWidth > 0.0 ? static_cast<double>(width()) / baseWidth : 1.0;
|
||||
const double scaleY = baseHeight > 0.0 ? static_cast<double>(height()) / baseHeight : 1.0;
|
||||
const QPointF localAnchor(
|
||||
static_cast<double>(width()) * base.anchorX + bubble.offsetX * scaleX,
|
||||
static_cast<double>(height()) * base.anchorY + bubble.offsetY * scaleY);
|
||||
|
||||
return frameGeometry().topLeft() + QPoint(qRound(localAnchor.x()), qRound(localAnchor.y()));
|
||||
}
|
||||
|
||||
void PetWindow::playState(const QString &stateName, bool centerWindow)
|
||||
@@ -869,9 +890,11 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
|
||||
|
||||
QSize PetWindow::animationTargetSize() const
|
||||
{
|
||||
const int sideLength = qRound(BaseAnimationTargetSize * m_appConfig.scale);
|
||||
const int boundedSideLength = qBound(BaseAnimationTargetSize / 2, sideLength, BaseAnimationTargetSize * 2);
|
||||
return QSize(boundedSideLength, boundedSideLength);
|
||||
const CharacterBase &base = m_characterPackage.base;
|
||||
const double totalScale = base.scale * m_appConfig.scale;
|
||||
return QSize(
|
||||
boundedAnimationTargetSide(static_cast<double>(base.width) * totalScale),
|
||||
boundedAnimationTargetSide(static_cast<double>(base.height) * totalScale));
|
||||
}
|
||||
|
||||
int PetWindow::effectiveAnimationFps(int fps) const
|
||||
|
||||
Reference in New Issue
Block a user