mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 07:11:29 +02:00
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.
This commit is contained in:
+116
-20
@@ -1,12 +1,37 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
// wowlet ambient scene — dusk sky, tiled grass, a running doge, bouncing WOW
|
// wowlet ambient scene — dusk sky, tiled grass, and a doge that runs the length
|
||||||
// coins, drifting money. All motion gates on `scene.reducedMotion` (exposed by
|
// of the field collecting WOW coins, then loops. All motion gates on
|
||||||
// the host SceneBackend); frozen it is the reduced-motion still.
|
// `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 {
|
Rectangle {
|
||||||
id: scene
|
id: scene
|
||||||
|
|
||||||
property bool reducedMotion: (typeof sceneCtx !== 'undefined' && sceneCtx) ? sceneCtx.reducedMotion : false
|
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 {
|
gradient: Gradient {
|
||||||
GradientStop { position: 0.0; color: "#160d2b" }
|
GradientStop { position: 0.0; color: "#160d2b" }
|
||||||
GradientStop { position: 0.5; color: "#5b2f6e" }
|
GradientStop { position: 0.5; color: "#5b2f6e" }
|
||||||
@@ -14,36 +39,107 @@ Rectangle {
|
|||||||
GradientStop { position: 1.0; color: "#f2adc9" }
|
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 {
|
AnimatedImage {
|
||||||
source: "qrc:/scene/flyingmoney.gif"; playing: !scene.reducedMotion
|
source: "qrc:/scene/flyingmoney.gif"
|
||||||
width: 70; height: 64; y: 30; x: 120; opacity: 0.9
|
playing: !scene.reducedMotion
|
||||||
NumberAnimation on x { running: !scene.reducedMotion; from: -80; to: scene.width; duration: 11000; loops: Animation.Infinite }
|
width: 60; height: 55
|
||||||
|
y: 4
|
||||||
|
x: scene.reducedMotion ? scene.width * 0.30 : -80 + scene.moneyProgress * (scene.width + 160)
|
||||||
|
opacity: 0.9
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
Repeater { model: Math.ceil(scene.width/180)+1
|
Repeater {
|
||||||
Image { source: "qrc:/scene/grass.png"; width: 180; height: 46; smooth: false } }
|
model: Math.ceil(scene.width / 180) + 1
|
||||||
|
Image { source: "qrc:/scene/grass.png"; width: 180; height: scene.grassHeight; smooth: false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: 3
|
model: scene.coinCount
|
||||||
AnimatedImage {
|
|
||||||
source: "qrc:/scene/goldcoin.gif"; playing: !scene.reducedMotion
|
Item {
|
||||||
width: 34; height: 34; x: 150 + index*170; y: scene.height*0.46
|
id: coin
|
||||||
SequentialAnimation on y {
|
width: scene.coinSize; height: scene.coinSize
|
||||||
running: !scene.reducedMotion; loops: Animation.Infinite
|
// Spread the coins over the field, clear of both ends of the run.
|
||||||
NumberAnimation { to: scene.height*0.36; duration: 1050 + index*170; easing.type: Easing.OutQuad }
|
x: scene.width * (0.22 + 0.66 * (scene.coinCount > 1 ? index / (scene.coinCount - 1) : 0.5)) - width / 2
|
||||||
NumberAnimation { to: scene.height*0.5; duration: 1050 + index*170; easing.type: Easing.InQuad }
|
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 {
|
AnimatedImage {
|
||||||
source: "qrc:/scene/dog.gif"; playing: !scene.reducedMotion
|
source: "qrc:/scene/dog.gif"
|
||||||
width: 104; height: 83; y: scene.height - 46 - 62; x: 70
|
playing: !scene.reducedMotion
|
||||||
NumberAnimation on x { running: !scene.reducedMotion; from: -110; to: scene.width; duration: 6500; loops: Animation.Infinite }
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
// SPDX-FileCopyrightText: The Monero Project
|
|
||||||
|
|
||||||
#include "IdleDoge.h"
|
|
||||||
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QMovie>
|
|
||||||
|
|
||||||
#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);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
// SPDX-FileCopyrightText: The Monero Project
|
|
||||||
|
|
||||||
#ifndef WOWLET_IDLEDOGE_H
|
|
||||||
#define WOWLET_IDLEDOGE_H
|
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
|
|
||||||
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
|
|
||||||
Reference in New Issue
Block a user