diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 84e062cd7..064b74932 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +44,13 @@ class QSizeF; +// Width the hovered card smooth-zooms to: exactly the hover panel picture size, so +// all four corners coincide when the panel swaps in over it. +static constexpr qreal HOVER_ZOOM_WIDTH = 270.0; +static constexpr int HOVER_ZOOM_IN_MS = 220; +static constexpr int HOVER_ZOOM_OUT_MS = 140; +static constexpr int FADE_DURATION_MS = 140; + AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) : ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0), bgColor(Qt::transparent), isHovered(false), realZValue(0) @@ -54,12 +63,36 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef connect(state, &CardState::cardInfoChanged, this, &AbstractCardItem::onCardInfoChanged); connect(state, &CardState::cardRefChanged, this, &AbstractCardItem::onCardRefChanged); connect(state, &CardState::tappedChanged, this, &AbstractCardItem::onTappedChanged); - connect(state, &CardState::facedownChanged, this, [this] { update(); }); + connect(state, &CardState::facedownChanged, this, [this](bool faceDown) { + update(); + emit faceDownChanged(faceDown); + }); connect(state, &CardState::colorChanged, this, [this] { cacheBgColor(); update(); }); + hoverZoom = new QPropertyAnimation(this, "cardScale", this); + hoverZoom->setEasingCurve(QEasingCurve::InOutCubic); + // Settling the unhover waits for the shrink-out to finish: the stacking override, + // the clip escape and the ancestor lift all stay put while the card is still large, + // so siblings and the clip container cannot cut through the shrinking card. + connect(hoverZoom, &QPropertyAnimation::finished, this, [this] { + if (!isHovered) { + setZValue(realZValue); + // Final repaint at the settled footprint: with no scale delta the animation + // emits no per-frame updates, and zones would keep a stale clip hole behind. + if (scene()) { + scene()->update(sceneBoundingRect()); + } + emit hoverShrinkFinished(this); + } + }); + + opacityAnim = new QPropertyAnimation(this, "cardOpacity", this); + opacityAnim->setDuration(FADE_DURATION_MS); + opacityAnim->setEasingCurve(QEasingCurve::OutCubic); + connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this, [this] { update(); }); connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, @@ -262,9 +295,38 @@ void AbstractCardItem::setHovered(bool _hovered) } isHovered = _hovered; - setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue); - setScale(_hovered && SettingsCache::instance().cardsDisplay().getScaleCards() ? 1.1 : 1); - setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0); + // The stacking override is one-directional: it applies the moment the hover starts + // and only drops once the shrink-out finishes (see the hoverZoom finished handler). + // Restoring it here would let siblings paint over the still-enlarged card. + if (_hovered) { + setZValue(ZValues::HOVERED_CARD); + } + // Scale about the center in both directions: at scale 1 the origin is moot, + // and keeping it centered avoids drift while shrinking back down. + setTransformOriginPoint(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F); + + // Smooth-zoom up to the hover picture size (instead of the old 1.1 snap) so the + // hover panel can then swap in over the zoomed card with matching corners. + // The target is constant ON-SCREEN pixels: divide out the board view zoom so + // fullscreen (or zoomed) boards don't overshoot while the fixed-size panel matches. + qreal target = 1.0; + if (_hovered && SettingsCache::instance().cardsDisplay().getScaleCards()) { + const qreal w = boundingRect().width(); + qreal viewScale = 1.0; + if (scene() && !scene()->views().isEmpty()) { + if (const auto *view = scene()->views().first()) { + viewScale = qAbs(view->transform().m11()); + } + } + if (w > 0.0 && viewScale > 0.0) { + target = qBound(1.0, HOVER_ZOOM_WIDTH / (w * viewScale), 6.0); + } + } + hoverZoom->stop(); + hoverZoom->setStartValue(scale()); + hoverZoom->setEndValue(target); + hoverZoom->setDuration(_hovered ? HOVER_ZOOM_IN_MS : HOVER_ZOOM_OUT_MS); + hoverZoom->start(); update(); } @@ -273,6 +335,59 @@ void AbstractCardItem::setColor(const QString &_color) state->setColor(_color); } +qreal AbstractCardItem::getCardScale() const +{ + return scale(); +} + +void AbstractCardItem::setCardScale(qreal _scale) +{ + if (qFuzzyCompare(_scale, scale())) { + return; + } + // The scale animates every frame: keep the scene index in step or hover + // picking tests a stale rect and the hover oscillates on/off by itself. + // Repaint the union of the old and new footprints, not just the card: zones + // dodge the zoomed card via a clip hole computed at paint time, and without + // a repaint they keep a stale hole — covering the card mid-animation and + // leaving a black rect behind once the hover ends. + const QRectF oldFootprint = sceneBoundingRect(); + prepareGeometryChange(); + setScale(_scale); + if (scene()) { + scene()->update(oldFootprint.united(sceneBoundingRect())); + } else { + update(); + } +} + +qreal AbstractCardItem::getCardOpacity() const +{ + return opacity(); +} + +void AbstractCardItem::setCardOpacity(qreal _opacity) +{ + setOpacity(_opacity); + update(); +} + +void AbstractCardItem::fadeCardOut() +{ + opacityAnim->stop(); + opacityAnim->setStartValue(opacity()); + opacityAnim->setEndValue(0.0); + opacityAnim->start(); +} + +void AbstractCardItem::fadeCardIn() +{ + opacityAnim->stop(); + opacityAnim->setStartValue(opacity()); + opacityAnim->setEndValue(1.0); + opacityAnim->start(); +} + void AbstractCardItem::cacheBgColor() { QChar colorChar; @@ -310,6 +425,7 @@ void AbstractCardItem::cacheBgColor() void AbstractCardItem::onTappedChanged(bool newTapped, bool canAnimate) { + emit tappedChanged(newTapped); if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) { static_cast(scene())->registerAnimationItem(this); } else { diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h index eee8ec743..9f506fb52 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -23,10 +23,13 @@ class PlayerLogic; class CardInfo; class QPainter; class QPoint; +class QPropertyAnimation; class AbstractCardItem : public ArrowTarget, public IAnimatedItem { Q_OBJECT + Q_PROPERTY(qreal cardScale READ getCardScale WRITE setCardScale) + Q_PROPERTY(qreal cardOpacity READ getCardOpacity WRITE setCardOpacity) protected: CardState *state; int tapAngle; @@ -37,6 +40,8 @@ private: qreal realZValue; bool pendingSelectionCollapse; int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings + QPropertyAnimation *hoverZoom = nullptr; + QPropertyAnimation *opacityAnim = nullptr; private slots: void pixmapUpdated(); void onCardInfoChanged(); @@ -57,6 +62,10 @@ signals: void playSelectedFaceDown(AbstractCardItem *card); void hideSelected(AbstractCardItem *card); void selectionChanged(AbstractCardItem *card, bool selected); + void tappedChanged(bool tapped); + void faceDownChanged(bool faceDown); + /** @brief Emitted when the hover shrink-out animation runs to completion while unhovered. */ + void hoverShrinkFinished(AbstractCardItem *card); public: enum @@ -116,6 +125,13 @@ public: } void setRealZValue(qreal _zValue); void setHovered(bool _hovered); + qreal getCardScale() const; + void setCardScale(qreal scale); + qreal getCardOpacity() const; + void setCardOpacity(qreal opacity); + /** @brief Fades the card out under the hover panel, or back in when it closes. */ + void fadeCardOut(); + void fadeCardIn(); bool getIsHovered() const { return isHovered; diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index 02c0427d7..9cc57f2b6 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -117,6 +117,11 @@ void CardItem::deleteLater() void CardItem::setZone(CardZoneLogic *_zone) { state->setZone(_zone); + // A card faded out under the open hover panel must come back as soon as it changes + // zones; otherwise it stays invisible at its destination until the panel closes. + if (opacity() < 1.0) { + fadeCardIn(); + } } void CardItem::retranslateUi() diff --git a/cockatrice/src/game_graphics/z_values.h b/cockatrice/src/game_graphics/z_values.h index c6e7f2c8a..8e198102b 100644 --- a/cockatrice/src/game_graphics/z_values.h +++ b/cockatrice/src/game_graphics/z_values.h @@ -34,6 +34,10 @@ constexpr qreal OVERLAY_BASE = ZValueLayerManager::OVERLAY_BASE; // Overlay layer Z-values for items that should appear above normal cards constexpr qreal HOVERED_CARD = ZValueLayerManager::overlayZValue(1.0); +// Lifts the hovered card's whole player area above sibling areas (and their zone +// decorations) while hovering. One level only: Qt compares subtrees at their highest +// differing ancestor, so the top ancestor decides. Below arrows/views/drags on purpose. +constexpr qreal HOVERED_AREA = ZValueLayerManager::overlayZValue(2.0); constexpr qreal ARROWS = ZValueLayerManager::overlayZValue(3.0); constexpr qreal ZONE_VIEW_WIDGET = ZValueLayerManager::overlayZValue(4.0); constexpr qreal DRAG_ITEM = ZValueLayerManager::overlayZValue(5.0);