From 636b0ff872cc11ad8082671252ad3dc8adfe5385 Mon Sep 17 00:00:00 2001 From: Ysm-04 Date: Sat, 30 May 2026 13:44:37 +0800 Subject: [PATCH] fix: apply character bubble anchors --- shiroko/character.json | 2 +- src/character/CharacterPackage.h | 18 +++++++- src/character/CharacterPackageLoader.cpp | 59 ++++++++++++++++++++++++ src/ui/ChatBubble.cpp | 4 +- src/ui/PetWindow.cpp | 33 +++++++++++-- 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/shiroko/character.json b/shiroko/character.json index 990036d..2865528 100644 --- a/shiroko/character.json +++ b/shiroko/character.json @@ -15,7 +15,7 @@ }, "bubble": { "offsetX": 0, - "offsetY": -180 + "offsetY": -475 }, "states": { "idle": { diff --git a/src/character/CharacterPackage.h b/src/character/CharacterPackage.h index 53f70c8..a88037d 100644 --- a/src/character/CharacterPackage.h +++ b/src/character/CharacterPackage.h @@ -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 states; bool hasState(const QString &stateName) const; const CharacterState *state(const QString &stateName) const; }; - diff --git a/src/character/CharacterPackageLoader.cpp b/src/character/CharacterPackageLoader.cpp index abd82e3..fc3df6a 100644 --- a/src/character/CharacterPackageLoader.cpp +++ b/src/character/CharacterPackageLoader.cpp @@ -6,6 +6,7 @@ #include #include #include +#include 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()) { diff --git a/src/ui/ChatBubble.cpp b/src/ui/ChatBubble.cpp index 500cb09..475fd5d 100644 --- a/src/ui/ChatBubble.cpp +++ b/src/ui/ChatBubble.cpp @@ -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()); } diff --git a/src/ui/PetWindow.cpp b/src/ui/PetWindow.cpp index 2d7cff5..1daeec3 100644 --- a/src/ui/PetWindow.cpp +++ b/src/ui/PetWindow.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -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(MinAnimationTargetSide), + sideLength, + static_cast(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(base.width) : static_cast(width()); + const double baseHeight = base.height > 0 ? static_cast(base.height) : static_cast(height()); + const double scaleX = baseWidth > 0.0 ? static_cast(width()) / baseWidth : 1.0; + const double scaleY = baseHeight > 0.0 ? static_cast(height()) / baseHeight : 1.0; + const QPointF localAnchor( + static_cast(width()) * base.anchorX + bubble.offsetX * scaleX, + static_cast(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(base.width) * totalScale), + boundedAnimationTargetSide(static_cast(base.height) * totalScale)); } int PetWindow::effectiveAnimationFps(int fps) const