From 2a54663f76fba6bb8c8ced2132fcaad5fbe35ed8 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 31 Aug 2026 12:14:46 -0400 Subject: [PATCH] fix(scene): the doge collects the coins, and no more slow first lap Three things you spotted on the History banner: - Slow first lap that restarted at speed. The dog and the money animated `to: scene.width`, and the QQuickWidget starts at a placeholder width: the first lap crawled across the wrong distance, then the real width arrived, re-bound `to`, and restarted the animation. Motion is now driven by a unitless 0..1 progress and mapped onto the current width each frame, so a resize repositions without restarting. - Coins sat mid-sky and clipped the top border. They now rest on the grass with a low bob, at the doge's pickup height. - The doge collects them. A coin is collected once the doge's snout reaches its centre: it pops, fades, and floats a "+1". The collected state is derived from the run position, so the coins simply come back when the lap restarts, with nothing to reset. Reduced motion still gets a static field: doge parked at the left, every coin present, money parked in the sky. Verified by headless qml6 render at banner (1000x140, 620x120) and lock-screen (900x380) sizes, motion and reduced-motion, with no QML warnings. --- src/assets/qml/scene.qml | 136 +++++++++++++++++++++++++++++++++------ src/widgets/IdleDoge.cpp | 46 ------------- src/widgets/IdleDoge.h | 25 ------- 3 files changed, 116 insertions(+), 91 deletions(-) delete mode 100644 src/widgets/IdleDoge.cpp delete mode 100644 src/widgets/IdleDoge.h diff --git a/src/assets/qml/scene.qml b/src/assets/qml/scene.qml index 2edc2f3b..03bf994c 100644 --- a/src/assets/qml/scene.qml +++ b/src/assets/qml/scene.qml @@ -1,12 +1,37 @@ import QtQuick -// wowlet ambient scene — dusk sky, tiled grass, a running doge, bouncing WOW -// coins, drifting money. All motion gates on `scene.reducedMotion` (exposed by -// the host SceneBackend); frozen it is the reduced-motion still. +// wowlet ambient scene — dusk sky, tiled grass, and a doge that runs the length +// of the field collecting WOW coins, then loops. All motion gates on +// `scene.reducedMotion` (exposed by the host SceneBackend); frozen it is the +// reduced-motion still. +// +// Motion is driven by a unitless 0..1 `runProgress` rather than by animating +// straight to `scene.width`: an animation whose `to:` is bound to the width +// restarts every time the host widget resizes, which showed up as a slow first +// lap that snapped back to the start once the real width arrived. Rectangle { id: scene + property bool reducedMotion: (typeof sceneCtx !== 'undefined' && sceneCtx) ? sceneCtx.reducedMotion : false + // Ground plane and the doge's run. + readonly property int grassHeight: 46 + readonly property int dogWidth: 104 + readonly property int dogHeight: 83 + readonly property real groundY: scene.height - grassHeight + readonly property real runStart: -dogWidth - 10 + readonly property real runEnd: scene.width + 10 + + property real runProgress: 0.0 + readonly property real dogX: runStart + runProgress * (runEnd - runStart) + // The doge collects a coin once its snout reaches the coin's centre. + readonly property real snoutX: dogX + dogWidth * 0.78 + + // Coins rest on the grass so the run reads as a pickup, not a fly-by. + readonly property int coinSize: 34 + readonly property real coinRestY: groundY - coinSize + 8 + readonly property int coinCount: Math.max(3, Math.min(7, Math.floor(scene.width / 190))) + gradient: Gradient { GradientStop { position: 0.0; color: "#160d2b" } GradientStop { position: 0.5; color: "#5b2f6e" } @@ -14,36 +39,107 @@ Rectangle { GradientStop { position: 1.0; color: "#f2adc9" } } - Rectangle { x: parent.width*0.74; y: parent.height*0.14; width: 62; height: 62; radius: 31; color: "#ffe9b0"; opacity: 0.9 } + // Sun, low in the dusk sky. + Rectangle { + x: scene.width * 0.74; y: scene.height * 0.14 + width: 62; height: 62; radius: 31 + color: "#ffe9b0"; opacity: 0.9 + } + // Money drifting across the sky, on its own unitless lap. + property real moneyProgress: 0.0 AnimatedImage { - source: "qrc:/scene/flyingmoney.gif"; playing: !scene.reducedMotion - width: 70; height: 64; y: 30; x: 120; opacity: 0.9 - NumberAnimation on x { running: !scene.reducedMotion; from: -80; to: scene.width; duration: 11000; loops: Animation.Infinite } + source: "qrc:/scene/flyingmoney.gif" + playing: !scene.reducedMotion + width: 60; height: 55 + y: 4 + x: scene.reducedMotion ? scene.width * 0.30 : -80 + scene.moneyProgress * (scene.width + 160) + opacity: 0.9 } Row { anchors.bottom: parent.bottom - Repeater { model: Math.ceil(scene.width/180)+1 - Image { source: "qrc:/scene/grass.png"; width: 180; height: 46; smooth: false } } + Repeater { + model: Math.ceil(scene.width / 180) + 1 + Image { source: "qrc:/scene/grass.png"; width: 180; height: scene.grassHeight; smooth: false } + } } Repeater { - model: 3 - AnimatedImage { - source: "qrc:/scene/goldcoin.gif"; playing: !scene.reducedMotion - width: 34; height: 34; x: 150 + index*170; y: scene.height*0.46 - SequentialAnimation on y { - running: !scene.reducedMotion; loops: Animation.Infinite - NumberAnimation { to: scene.height*0.36; duration: 1050 + index*170; easing.type: Easing.OutQuad } - NumberAnimation { to: scene.height*0.5; duration: 1050 + index*170; easing.type: Easing.InQuad } + model: scene.coinCount + + Item { + id: coin + width: scene.coinSize; height: scene.coinSize + // Spread the coins over the field, clear of both ends of the run. + x: scene.width * (0.22 + 0.66 * (scene.coinCount > 1 ? index / (scene.coinCount - 1) : 0.5)) - width / 2 + y: scene.coinRestY + + // Purely derived from the run, so a lap reset restores every coin + // with no bookkeeping. + readonly property bool collected: !scene.reducedMotion && scene.snoutX >= x + width / 2 + + AnimatedImage { + id: coinImage + width: parent.width; height: parent.height + source: "qrc:/scene/goldcoin.gif" + playing: !scene.reducedMotion && !coin.collected + opacity: coin.collected ? 0.0 : 1.0 + scale: coin.collected ? 1.6 : 1.0 + // Idle bob, low over the grass. + SequentialAnimation on y { + running: !scene.reducedMotion && !coin.collected + loops: Animation.Infinite + NumberAnimation { to: -11; duration: 950 + index * 130; easing.type: Easing.OutQuad } + NumberAnimation { to: 0; duration: 950 + index * 130; easing.type: Easing.InQuad } + } + Behavior on opacity { NumberAnimation { duration: 260 } } + Behavior on scale { NumberAnimation { duration: 260; easing.type: Easing.OutBack } } + } + + // Pickup pop. + Text { + id: popText + anchors.horizontalCenter: parent.horizontalCenter + text: "+1" + color: "#ffd257" + style: Text.Outline + styleColor: "#2a1636" + font.bold: true + font.pixelSize: 18 + opacity: 0.0 + y: coin.collected ? -44 : -6 + Behavior on y { NumberAnimation { duration: 900; easing.type: Easing.OutQuad } } + SequentialAnimation on opacity { + running: coin.collected + NumberAnimation { to: 1.0; duration: 90 } + PauseAnimation { duration: 420 } + NumberAnimation { to: 0.0; duration: 340 } + } } } } AnimatedImage { - source: "qrc:/scene/dog.gif"; playing: !scene.reducedMotion - width: 104; height: 83; y: scene.height - 46 - 62; x: 70 - NumberAnimation on x { running: !scene.reducedMotion; from: -110; to: scene.width; duration: 6500; loops: Animation.Infinite } + source: "qrc:/scene/dog.gif" + playing: !scene.reducedMotion + width: scene.dogWidth; height: scene.dogHeight + y: scene.groundY - scene.dogHeight + 21 + x: scene.reducedMotion ? 8 : scene.dogX + } + + // One lap: run the field, then start over with the coins restored. + NumberAnimation on runProgress { + running: !scene.reducedMotion + from: 0.0; to: 1.0 + duration: 8000 + loops: Animation.Infinite + } + + NumberAnimation on moneyProgress { + running: !scene.reducedMotion + from: 0.0; to: 1.0 + duration: 13000 + loops: Animation.Infinite } } diff --git a/src/widgets/IdleDoge.cpp b/src/widgets/IdleDoge.cpp deleted file mode 100644 index 4abd5fb8..00000000 --- a/src/widgets/IdleDoge.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: The Monero Project - -#include "IdleDoge.h" - -#include -#include - -#include "utils/config.h" - -IdleDoge::IdleDoge(QWidget *parent, int height) - : QLabel(parent) - , m_movie(new QMovie(":/assets/images/dog_sitting.gif", QByteArray(), this)) -{ - m_movie->jumpToFrame(0); - const QSize frame = m_movie->currentImage().size(); - if (frame.height() > 0) - m_movie->setScaledSize(QSize(frame.width() * height / frame.height(), height)); - this->setMovie(m_movie); - this->setAttribute(Qt::WA_TransparentForMouseEvents); // never block the form - this->adjustSize(); - - if (conf()->get(Config::reducedMotion).toBool()) - m_movie->jumpToFrame(0); // still frame - else - m_movie->start(); - - if (parent) { - parent->installEventFilter(this); - this->reposition(); - this->raise(); - } -} - -void IdleDoge::reposition() { - if (auto *p = parentWidget()) - this->move(p->width() - this->width() - 14, p->height() - this->height() - 14); -} - -bool IdleDoge::eventFilter(QObject *o, QEvent *e) { - if (o == parentWidget() && (e->type() == QEvent::Resize || e->type() == QEvent::Show)) { - this->reposition(); - this->raise(); - } - return QLabel::eventFilter(o, e); -} diff --git a/src/widgets/IdleDoge.h b/src/widgets/IdleDoge.h deleted file mode 100644 index 8f09d241..00000000 --- a/src/widgets/IdleDoge.h +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: The Monero Project - -#ifndef WOWLET_IDLEDOGE_H -#define WOWLET_IDLEDOGE_H - -#include - -class QMovie; - -// wowlet: a small sitting doge that floats, click-through, in the bottom-right -// corner of its parent — the "most quiet" ambient idle for task screens (Send, -// Receive). Freezes to a still frame when "Prefer reduced motion" is set. -class IdleDoge : public QLabel { -Q_OBJECT -public: - explicit IdleDoge(QWidget *parent, int height = 44); -protected: - bool eventFilter(QObject *o, QEvent *e) override; -private: - void reposition(); - QMovie *m_movie; -}; - -#endif //WOWLET_IDLEDOGE_H